declare_schema 0.1.3 → 0.2.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.
@@ -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
+