declare_schema 0.1.3 → 0.2.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,11 +3,15 @@
3
3
  require_relative '../../../lib/declare_schema/field_declaration_dsl'
4
4
 
5
5
  RSpec.describe DeclareSchema::FieldDeclarationDsl do
6
- class TestModel < ActiveRecord::Base
7
- fields do
8
- name :string, limit: 127
6
+ before do
7
+ load File.expand_path('prepare_testapp.rb', __dir__)
9
8
 
10
- timestamps
9
+ class TestModel < ActiveRecord::Base
10
+ fields do
11
+ name :string, limit: 127
12
+
13
+ timestamps
14
+ end
11
15
  end
12
16
  end
13
17
 
@@ -0,0 +1,57 @@
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
+ it "generates nested models" do
9
+ Rails::Generators.invoke('declare_schema:model', %w[alpha/beta one:string two:integer])
10
+
11
+ expect(File.exist?('app/models/alpha/beta.rb')).to be_truthy
12
+
13
+ expect(File.read('app/models/alpha/beta.rb')).to eq(<<~EOS)
14
+ class Alpha::Beta < #{active_record_base_class}
15
+
16
+ fields do
17
+ one :string, limit: 255
18
+ two :integer
19
+ end
20
+
21
+ end
22
+ EOS
23
+
24
+ expect(File.read('app/models/alpha.rb')).to eq(<<~EOS)
25
+ module Alpha
26
+ def self.table_name_prefix
27
+ 'alpha_'
28
+ end
29
+ end
30
+ EOS
31
+
32
+ expect(File.read('test/models/alpha/beta_test.rb')).to eq(<<~EOS)
33
+ require 'test_helper'
34
+
35
+ class Alpha::BetaTest < ActiveSupport::TestCase
36
+ # test "the truth" do
37
+ # assert true
38
+ # end
39
+ end
40
+ EOS
41
+
42
+ expect(File.exist?('test/fixtures/alpha/beta.yml')).to be_truthy
43
+
44
+ $LOAD_PATH << "#{TESTAPP_PATH}/app/models"
45
+
46
+ expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
47
+
48
+ expect(File.exist?('db/schema.rb')).to be_truthy
49
+
50
+ expect(File.exist?("db/development.sqlite3") || File.exist?("db/test.sqlite3")).to be_truthy
51
+
52
+ module Alpha; end
53
+ require 'alpha/beta'
54
+
55
+ expect { Alpha::Beta }.to_not raise_exception
56
+ end
57
+ end
@@ -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,686 @@
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, down = Generators::DeclareSchema::Migration::Migrator.run
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, down = Generators::DeclareSchema::Migration::Migrator.run
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, down = Generators::DeclareSchema::Migration::Migrator.run
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, down = Generators::DeclareSchema::Migration::Migrator.run
198
+ expect(up).to eq(<<~EOS.strip)
199
+ add_column :adverts, :notes, :text, null: false
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, 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, 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, down = Generators::DeclareSchema::Migration::Migrator.run
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
+ belongs_to :category
282
+ end
283
+
284
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
285
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
286
+ add_column :adverts, :category_id, :integer, limit: 8, null: false
287
+ add_index :adverts, [:category_id], name: 'on_category_id'
288
+ EOS
289
+ expect(down.sub(/\n+/, "\n")).to eq(<<~EOS.strip)
290
+ remove_column :adverts, :category_id
291
+ remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid
292
+ EOS
293
+
294
+ Advert.field_specs.delete(:category_id)
295
+ Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
296
+
297
+ # If you specify a custom foreign key, the migration generator observes that:
298
+
299
+ class Category < ActiveRecord::Base; end
300
+ class Advert < ActiveRecord::Base
301
+ belongs_to :category, foreign_key: "c_id", class_name: 'Category'
302
+ end
303
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
304
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
305
+ add_column :adverts, :c_id, :integer, limit: 8, null: false
306
+ add_index :adverts, [:c_id], name: 'on_c_id'
307
+ EOS
308
+
309
+ Advert.field_specs.delete(:c_id)
310
+ Advert.index_specs.delete_if { |spec| spec.fields == ["c_id"] }
311
+
312
+ # You can avoid generating the index by specifying `index: false`
313
+
314
+ class Category < ActiveRecord::Base; end
315
+ class Advert < ActiveRecord::Base
316
+ belongs_to :category, index: false
317
+ end
318
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
319
+ expect(up.gsub(/\n+/, "\n")).to eq("add_column :adverts, :category_id, :integer, limit: 8, null: false")
320
+
321
+ Advert.field_specs.delete(:category_id)
322
+ Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
323
+
324
+ # You can specify the index name with :index
325
+
326
+ class Category < ActiveRecord::Base; end
327
+ class Advert < ActiveRecord::Base
328
+ belongs_to :category, index: 'my_index'
329
+ end
330
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
331
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
332
+ add_column :adverts, :category_id, :integer, limit: 8, null: false
333
+ add_index :adverts, [:category_id], name: 'my_index'
334
+ EOS
335
+
336
+ Advert.field_specs.delete(:category_id)
337
+ Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
338
+
339
+ ### Timestamps and Optimimistic Locking
340
+
341
+ # `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
342
+ # Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
343
+
344
+ class Advert < ActiveRecord::Base
345
+ fields do
346
+ timestamps
347
+ optimistic_lock
348
+ end
349
+ end
350
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
351
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
352
+ add_column :adverts, :created_at, :datetime
353
+ add_column :adverts, :updated_at, :datetime
354
+ add_column :adverts, :lock_version, :integer, null: false, default: 1
355
+ EOS
356
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
357
+ remove_column :adverts, :created_at
358
+ remove_column :adverts, :updated_at
359
+ remove_column :adverts, :lock_version
360
+ EOS
361
+
362
+ Advert.field_specs.delete(:updated_at)
363
+ Advert.field_specs.delete(:created_at)
364
+ Advert.field_specs.delete(:lock_version)
365
+
366
+ ### Indices
367
+
368
+ # You can add an index to a field definition
369
+
370
+ class Advert < ActiveRecord::Base
371
+ fields do
372
+ title :string, index: true, limit: 255, null: true
373
+ end
374
+ end
375
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
376
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
377
+ add_column :adverts, :title, :string, limit: 255
378
+ add_index :adverts, [:title], name: 'on_title'
379
+ EOS
380
+
381
+ Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
382
+
383
+ # You can ask for a unique index
384
+
385
+ class Advert < ActiveRecord::Base
386
+ fields do
387
+ title :string, index: true, unique: true, null: true, limit: 255
388
+ end
389
+ end
390
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
391
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
392
+ add_column :adverts, :title, :string, limit: 255
393
+ add_index :adverts, [:title], unique: true, name: 'on_title'
394
+ EOS
395
+
396
+ Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
397
+
398
+ # You can specify the name for the index
399
+
400
+ class Advert < ActiveRecord::Base
401
+ fields do
402
+ title :string, index: 'my_index', limit: 255, null: true
403
+ end
404
+ end
405
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
406
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
407
+ add_column :adverts, :title, :string, limit: 255
408
+ add_index :adverts, [:title], name: 'my_index'
409
+ EOS
410
+
411
+ Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
412
+
413
+ # You can ask for an index outside of the fields block
414
+
415
+ class Advert < ActiveRecord::Base
416
+ index :title
417
+ end
418
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
419
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
420
+ add_column :adverts, :title, :string, limit: 255
421
+ add_index :adverts, [:title], name: 'on_title'
422
+ EOS
423
+
424
+ Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
425
+
426
+ # The available options for the index function are `:unique` and `:name`
427
+
428
+ class Advert < ActiveRecord::Base
429
+ index :title, unique: true, name: 'my_index'
430
+ end
431
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
432
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
433
+ add_column :adverts, :title, :string, limit: 255
434
+ add_index :adverts, [:title], unique: true, name: 'my_index'
435
+ EOS
436
+
437
+ Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
438
+
439
+ # You can create an index on more than one field
440
+
441
+ class Advert < ActiveRecord::Base
442
+ index [:title, :category_id]
443
+ end
444
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
445
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
446
+ add_column :adverts, :title, :string, limit: 255
447
+ add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'
448
+ EOS
449
+
450
+ Advert.index_specs.delete_if { |spec| spec.fields==["title", "category_id"] }
451
+
452
+ # Finally, you can specify that the migration generator should completely ignore an
453
+ # index by passing its name to ignore_index in the model.
454
+ # This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
455
+
456
+ ### Rename a table
457
+
458
+ # 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.
459
+
460
+ class Advert < ActiveRecord::Base
461
+ self.table_name = "ads"
462
+ fields do
463
+ title :string, limit: 255, null: true
464
+ body :text, null: true
465
+ end
466
+ end
467
+
468
+ Advert.connection.schema_cache.clear!
469
+ Advert.reset_column_information
470
+
471
+ up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")
472
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
473
+ rename_table :adverts, :ads
474
+ add_column :ads, :title, :string, limit: 255
475
+ add_column :ads, :body, :text
476
+ add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
477
+ EOS
478
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
479
+ remove_column :ads, :title
480
+ remove_column :ads, :body
481
+ rename_table :ads, :adverts
482
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
483
+ EOS
484
+
485
+ # Set the table name back to what it should be and confirm we're in sync:
486
+
487
+ Advert.field_specs.delete(:title)
488
+ Advert.field_specs.delete(:body)
489
+ class Advert < ActiveRecord::Base
490
+ self.table_name = "adverts"
491
+ end
492
+
493
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
494
+
495
+ ### Rename a table
496
+
497
+ # 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.
498
+
499
+ nuke_model_class(Advert)
500
+
501
+ class Advertisement < ActiveRecord::Base
502
+ fields do
503
+ title :string, limit: 255, null: true
504
+ body :text, null: true
505
+ end
506
+ end
507
+ up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")
508
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
509
+ rename_table :adverts, :advertisements
510
+ add_column :advertisements, :title, :string, limit: 255
511
+ add_column :advertisements, :body, :text
512
+ remove_column :advertisements, :name
513
+ add_index :advertisements, [:id], unique: true, name: 'PRIMARY_KEY'
514
+ EOS
515
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
516
+ remove_column :advertisements, :title
517
+ remove_column :advertisements, :body
518
+ add_column :adverts, :name, :string, limit: 255
519
+ rename_table :advertisements, :adverts
520
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
521
+ EOS
522
+
523
+ ### Drop a table
524
+
525
+ nuke_model_class(Advertisement)
526
+
527
+ # If you delete a model, the migration generator will create a `drop_table` migration.
528
+
529
+ # Dropping tables is where the automatic down-migration really comes in handy:
530
+
531
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
532
+ expect(up).to eq("drop_table :adverts")
533
+ expect(down.gsub(/,.*/m, '')).to eq("create_table \"adverts\"")
534
+
535
+ ## STI
536
+
537
+ ### Adding an STI subclass
538
+
539
+ # Adding a subclass or two should introduce the 'type' column and no other changes
540
+
541
+ class Advert < ActiveRecord::Base
542
+ fields do
543
+ body :text, null: true
544
+ title :string, default: "Untitled", limit: 255, null: true
545
+ end
546
+ end
547
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
548
+ ActiveRecord::Migration.class_eval(up)
549
+
550
+ class FancyAdvert < Advert
551
+ end
552
+ class SuperFancyAdvert < FancyAdvert
553
+ end
554
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
555
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
556
+ add_column :adverts, :type, :string, limit: 255
557
+ add_index :adverts, [:type], name: 'on_type'
558
+ EOS
559
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
560
+ remove_column :adverts, :type
561
+ remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid
562
+ EOS
563
+
564
+ Advert.field_specs.delete(:type)
565
+ nuke_model_class(SuperFancyAdvert)
566
+ nuke_model_class(FancyAdvert)
567
+ Advert.index_specs.delete_if { |spec| spec.fields==["type"] }
568
+
569
+ ## Coping with multiple changes
570
+
571
+ # The migration generator is designed to create complete migrations even if many changes to the models have taken place.
572
+
573
+ # First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
574
+
575
+ ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
576
+ Advert.connection.schema_cache.clear!
577
+ Advert.reset_column_information
578
+
579
+ expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
580
+ to eq(["adverts"])
581
+ expect(Advert.columns.map(&:name).sort).to eq(["body", "id", "title"])
582
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
583
+
584
+
585
+ ### Rename a column and change the default
586
+
587
+ Advert.field_specs.clear
588
+
589
+ class Advert < ActiveRecord::Base
590
+ fields do
591
+ name :string, default: "No Name", limit: 255, null: true
592
+ body :text, null: true
593
+ end
594
+ end
595
+ up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })
596
+ expect(up).to eq(<<~EOS.strip)
597
+ rename_column :adverts, :title, :name
598
+ change_column :adverts, :name, :string, limit: 255, default: \"No Name\"
599
+ EOS
600
+
601
+ expect(down).to eq(<<~EOS.strip)
602
+ rename_column :adverts, :name, :title
603
+ change_column :adverts, :title, :string, limit: 255, default: \"Untitled\"
604
+ EOS
605
+
606
+ ### Rename a table and add a column
607
+
608
+ nuke_model_class(Advert)
609
+ class Ad < ActiveRecord::Base
610
+ fields do
611
+ title :string, default: "Untitled", limit: 255
612
+ body :text, null: true
613
+ created_at :datetime
614
+ end
615
+ end
616
+ up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)
617
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
618
+ rename_table :adverts, :ads
619
+ add_column :ads, :created_at, :datetime, null: false
620
+ change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
621
+ add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
622
+ EOS
623
+
624
+ class Advert < ActiveRecord::Base
625
+ fields do
626
+ body :text, null: true
627
+ title :string, default: "Untitled", limit: 255, null: true
628
+ end
629
+ end
630
+
631
+ ## Legacy Keys
632
+
633
+ # DeclareSchema has some support for legacy keys.
634
+
635
+ nuke_model_class(Ad)
636
+
637
+ class Advert < ActiveRecord::Base
638
+ fields do
639
+ body :text, null: true
640
+ end
641
+ self.primary_key = "advert_id"
642
+ end
643
+ up, _down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })
644
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
645
+ rename_column :adverts, :id, :advert_id
646
+ add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY_KEY'
647
+ EOS
648
+
649
+ nuke_model_class(Advert)
650
+ ActiveRecord::Base.connection.execute("drop table `adverts`;")
651
+
652
+ ## DSL
653
+
654
+ # The DSL allows lambdas and constants
655
+
656
+ class User < ActiveRecord::Base
657
+ fields do
658
+ company :string, limit: 255, ruby_default: -> { "BigCorp" }
659
+ end
660
+ end
661
+ expect(User.field_specs.keys).to eq(['company'])
662
+ expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
663
+
664
+ ## validates
665
+
666
+ # DeclareSchema can accept a validates hash in the field options.
667
+
668
+ class Ad < ActiveRecord::Base
669
+ class << self
670
+ def validates(field_name, options)
671
+ end
672
+ end
673
+ end
674
+ expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
675
+ class Ad < ActiveRecord::Base
676
+ fields do
677
+ company :string, limit: 255, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
678
+ end
679
+ self.primary_key = "advert_id"
680
+ end
681
+ up, _down = Generators::DeclareSchema::Migration::Migrator.run
682
+ ActiveRecord::Migration.class_eval(up)
683
+ expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
684
+ end
685
+ end
686
+