declare_schema 0.14.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -54,1171 +54,6 @@ RSpec.describe 'DeclareSchema Migration Generator' do
54
54
  end
55
55
  end
56
56
 
57
- context 'Using fields' do
58
- # DeclareSchema - Migration Generator
59
- it 'generates migrations' do
60
- ## The migration generator -- introduction
61
-
62
- expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
63
-
64
- class Advert < ActiveRecord::Base
65
- end
66
-
67
- expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
68
-
69
- Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
70
-
71
- Advert.connection.schema_cache.clear!
72
- Advert.reset_column_information
73
-
74
- class Advert < ActiveRecord::Base
75
- fields do
76
- name :string, limit: 250, null: true
77
- end
78
- end
79
-
80
- up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
81
- expect(migrations).to(
82
- migrate_up(<<~EOS.strip)
83
- create_table :adverts, id: :bigint#{create_table_charset_and_collation} do |t|
84
- t.string :name, limit: 250, null: true#{charset_and_collation}
85
- end
86
- EOS
87
- .and migrate_down("drop_table :adverts")
88
- )
89
- end
90
-
91
- ActiveRecord::Migration.class_eval(up)
92
- expect(Advert.columns.map(&:name)).to eq(["id", "name"])
93
-
94
- class Advert < ActiveRecord::Base
95
- fields do
96
- name :string, limit: 250, null: true
97
- body :text, null: true
98
- published_at :datetime, null: true
99
- end
100
- end
101
-
102
- Advert.connection.schema_cache.clear!
103
- Advert.reset_column_information
104
-
105
- expect(migrate).to(
106
- migrate_up(<<~EOS.strip)
107
- add_column :adverts, :body, :text#{text_limit}, null: true#{charset_and_collation}
108
- add_column :adverts, :published_at, :datetime, null: true
109
- EOS
110
- .and migrate_down(<<~EOS.strip)
111
- remove_column :adverts, :published_at
112
- remove_column :adverts, :body
113
- EOS
114
- )
115
-
116
- Advert.field_specs.clear # not normally needed
117
- class Advert < ActiveRecord::Base
118
- fields do
119
- name :string, limit: 250, null: true
120
- body :text, null: true
121
- end
122
- end
123
-
124
- expect(migrate).to(
125
- migrate_up("remove_column :adverts, :published_at").and(
126
- migrate_down("add_column :adverts, :published_at, :datetime#{datetime_precision}, null: true")
127
- )
128
- )
129
-
130
- nuke_model_class(Advert)
131
- class Advert < ActiveRecord::Base
132
- fields do
133
- title :string, limit: 250, null: true
134
- body :text, null: true
135
- end
136
- end
137
-
138
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
139
- migrate_up(<<~EOS.strip)
140
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
141
- remove_column :adverts, :name
142
- EOS
143
- .and migrate_down(<<~EOS.strip)
144
- add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
145
- remove_column :adverts, :title
146
- EOS
147
- )
148
-
149
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })).to(
150
- migrate_up("rename_column :adverts, :name, :title").and(
151
- migrate_down("rename_column :adverts, :title, :name")
152
- )
153
- )
154
-
155
- migrate
156
-
157
- class Advert < ActiveRecord::Base
158
- fields do
159
- title :text, null: true
160
- body :text, null: true
161
- end
162
- end
163
-
164
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
165
- migrate_up("change_column :adverts, :title, :text#{text_limit}, null: true#{charset_and_collation}").and(
166
- migrate_down("change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}")
167
- )
168
- )
169
-
170
- class Advert < ActiveRecord::Base
171
- fields do
172
- title :string, default: "Untitled", limit: 250, null: true
173
- body :text, null: true
174
- end
175
- end
176
-
177
- expect(migrate).to(
178
- migrate_up(<<~EOS.strip)
179
- change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
180
- EOS
181
- .and migrate_down(<<~EOS.strip)
182
- change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
183
- EOS
184
- )
185
-
186
- ### Limits
187
-
188
- class Advert < ActiveRecord::Base
189
- fields do
190
- price :integer, null: true, limit: 2
191
- end
192
- end
193
-
194
- up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
195
- expect(migrations).to migrate_up("add_column :adverts, :price, :integer, limit: 2, null: true")
196
- end
197
-
198
- # Now run the migration, then change the limit:
199
-
200
- ActiveRecord::Migration.class_eval(up)
201
- class Advert < ActiveRecord::Base
202
- fields do
203
- price :integer, null: true, limit: 3
204
- end
205
- end
206
-
207
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
208
- migrate_up(<<~EOS.strip)
209
- change_column :adverts, :price, :integer, limit: 3, null: true
210
- EOS
211
- .and migrate_down(<<~EOS.strip)
212
- change_column :adverts, :price, :integer, limit: 2, null: true
213
- EOS
214
- )
215
-
216
- ActiveRecord::Migration.class_eval("remove_column :adverts, :price")
217
- class Advert < ActiveRecord::Base
218
- fields do
219
- price :decimal, precision: 4, scale: 1, null: true
220
- end
221
- end
222
-
223
- # Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
224
- # allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
225
- # If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
226
- # that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
227
-
228
- if defined?(SQLite3)
229
- expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_falsey
230
- end
231
-
232
- class Advert < ActiveRecord::Base
233
- fields do
234
- notes :text
235
- description :text, limit: 30000
236
- end
237
- end
238
-
239
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
240
- migrate_up(<<~EOS.strip)
241
- add_column :adverts, :price, :decimal, precision: 4, scale: 1, null: true
242
- add_column :adverts, :notes, :text#{text_limit}, null: false#{charset_and_collation}
243
- add_column :adverts, :description, :text#{', limit: 65535' if defined?(Mysql2)}, null: false#{charset_and_collation}
244
- EOS
245
- )
246
-
247
- Advert.field_specs.delete :price
248
- Advert.field_specs.delete :notes
249
- Advert.field_specs.delete :description
250
-
251
- # In MySQL, limits are applied, rounded up:
252
-
253
- if defined?(Mysql2)
254
- expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
255
-
256
- class Advert < ActiveRecord::Base
257
- fields do
258
- notes :text
259
- description :text, limit: 250
260
- end
261
- end
262
-
263
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
264
- migrate_up(<<~EOS.strip)
265
- add_column :adverts, :notes, :text, limit: 4294967295, null: false#{charset_and_collation}
266
- add_column :adverts, :description, :text, limit: 255, null: false#{charset_and_collation}
267
- EOS
268
- )
269
-
270
- Advert.field_specs.delete :notes
271
-
272
- # Limits that are too high for MySQL will raise an exception.
273
-
274
- expect do
275
- class Advert < ActiveRecord::Base
276
- fields do
277
- notes :text
278
- description :text, limit: 0x1_0000_0000
279
- end
280
- end
281
- end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
282
-
283
- Advert.field_specs.delete :notes
284
-
285
- # And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
286
-
287
- # To start, we'll set the database schema for `description` to match the above limit of 250.
288
-
289
- Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
290
- Advert.connection.schema_cache.clear!
291
- Advert.reset_column_information
292
- expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
293
- to eq(["adverts"])
294
- expect(Advert.columns.map(&:name)).to eq(["id", "body", "title", "description"])
295
-
296
- # Now migrate to an unstated text limit:
297
-
298
- class Advert < ActiveRecord::Base
299
- fields do
300
- description :text
301
- end
302
- end
303
-
304
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
305
- migrate_up(<<~EOS.strip)
306
- change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
307
- EOS
308
- .and migrate_down(<<~EOS.strip)
309
- change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
310
- EOS
311
- )
312
-
313
- # And migrate to a stated text limit that is the same as the unstated one:
314
-
315
- class Advert < ActiveRecord::Base
316
- fields do
317
- description :text, limit: 0xffffffff
318
- end
319
- end
320
-
321
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
322
- migrate_up(<<~EOS.strip)
323
- change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
324
- EOS
325
- .and migrate_down(<<~EOS.strip)
326
- change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
327
- EOS
328
- )
329
- end
330
-
331
- Advert.field_specs.clear
332
- Advert.connection.schema_cache.clear!
333
- Advert.reset_column_information
334
- class Advert < ActiveRecord::Base
335
- fields do
336
- name :string, limit: 250, null: true
337
- end
338
- end
339
-
340
- up = Generators::DeclareSchema::Migration::Migrator.run.first
341
- ActiveRecord::Migration.class_eval up
342
-
343
- Advert.connection.schema_cache.clear!
344
- Advert.reset_column_information
345
-
346
- ### Foreign Keys
347
-
348
- # DeclareSchema extends the `belongs_to` macro so that it also declares the
349
- # foreign-key field. It also generates an index on the field.
350
-
351
- class Category < ActiveRecord::Base; end
352
- class Advert < ActiveRecord::Base
353
- fields do
354
- name :string, limit: 250, null: true
355
- end
356
- belongs_to :category
357
- end
358
-
359
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
360
- migrate_up(<<~EOS.strip)
361
- add_column :adverts, :category_id, :integer, limit: 8, null: false
362
- add_index :adverts, [:category_id], name: :on_category_id
363
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id" if defined?(Mysql2)}
364
- EOS
365
- .and migrate_down(<<~EOS.strip)
366
- #{"remove_foreign_key :adverts, name: :index_adverts_on_category_id" if defined?(Mysql2)}
367
- remove_index :adverts, name: :on_category_id
368
- remove_column :adverts, :category_id
369
- EOS
370
- )
371
-
372
- Advert.field_specs.delete(:category_id)
373
- Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
374
-
375
- # If you specify a custom foreign key, the migration generator observes that:
376
-
377
- class Category < ActiveRecord::Base; end
378
- class Advert < ActiveRecord::Base
379
- fields { }
380
- belongs_to :category, foreign_key: "c_id", class_name: 'Category'
381
- end
382
-
383
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
384
- migrate_up(<<~EOS.strip)
385
- add_column :adverts, :c_id, :integer, limit: 8, null: false
386
- add_index :adverts, [:c_id], name: :on_c_id
387
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
388
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
389
- EOS
390
- )
391
-
392
- Advert.field_specs.delete(:c_id)
393
- Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
394
-
395
- # You can avoid generating the index by specifying `index: false`
396
-
397
- class Category < ActiveRecord::Base; end
398
- class Advert < ActiveRecord::Base
399
- fields { }
400
- belongs_to :category, index: false
401
- end
402
-
403
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
404
- migrate_up(<<~EOS.strip)
405
- add_column :adverts, :category_id, :integer, limit: 8, null: false
406
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
407
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
408
- EOS
409
- )
410
-
411
- Advert.field_specs.delete(:category_id)
412
- Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
413
-
414
- # You can specify the index name with :index
415
-
416
- class Category < ActiveRecord::Base; end
417
- class Advert < ActiveRecord::Base
418
- fields { }
419
- belongs_to :category, index: 'my_index'
420
- end
421
-
422
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
423
- migrate_up(<<~EOS.strip)
424
- add_column :adverts, :category_id, :integer, limit: 8, null: false
425
- add_index :adverts, [:category_id], name: :my_index
426
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
427
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
428
- EOS
429
- )
430
-
431
- Advert.field_specs.delete(:category_id)
432
- Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
433
-
434
- ### Timestamps and Optimistic Locking
435
-
436
- # `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
437
- # Similarly, `lock_version` can be declared with the "shorthand" `optimistic_lock`.
438
-
439
- class Advert < ActiveRecord::Base
440
- fields do
441
- timestamps
442
- optimistic_lock
443
- end
444
- end
445
-
446
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
447
- migrate_up(<<~EOS.strip)
448
- add_column :adverts, :created_at, :datetime, null: true
449
- add_column :adverts, :updated_at, :datetime, null: true
450
- add_column :adverts, :lock_version, :integer#{lock_version_limit}, null: false, default: 1
451
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
452
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
453
- EOS
454
- .and migrate_down(<<~EOS.strip)
455
- #{"remove_foreign_key :adverts, name: :index_adverts_on_c_id\n" +
456
- "remove_foreign_key :adverts, name: :index_adverts_on_category_id" if defined?(Mysql2)}
457
- remove_column :adverts, :lock_version
458
- remove_column :adverts, :updated_at
459
- remove_column :adverts, :created_at
460
- EOS
461
- )
462
-
463
- Advert.field_specs.delete(:updated_at)
464
- Advert.field_specs.delete(:created_at)
465
- Advert.field_specs.delete(:lock_version)
466
-
467
- ### Indices
468
-
469
- # You can add an index to a field definition
470
-
471
- class Advert < ActiveRecord::Base
472
- fields do
473
- title :string, index: true, limit: 250, null: true
474
- end
475
- end
476
-
477
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
478
- migrate_up(<<~EOS.strip)
479
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
480
- add_index :adverts, [:title], name: :on_title
481
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
482
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
483
- EOS
484
- )
485
-
486
- Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
487
-
488
- # You can ask for a unique index
489
-
490
- class Advert < ActiveRecord::Base
491
- fields do
492
- title :string, index: true, unique: true, null: true, limit: 250
493
- end
494
- end
495
-
496
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
497
- migrate_up(<<~EOS.strip)
498
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
499
- add_index :adverts, [:title], name: :on_title, unique: true
500
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
501
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
502
- EOS
503
- )
504
-
505
- Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
506
-
507
- # You can specify the name for the index
508
-
509
- class Advert < ActiveRecord::Base
510
- fields do
511
- title :string, index: 'my_index', limit: 250, null: true
512
- end
513
- end
514
-
515
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
516
- migrate_up(<<~EOS.strip)
517
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
518
- add_index :adverts, [:title], name: :my_index
519
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
520
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
521
- EOS
522
- )
523
-
524
- Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
525
-
526
- # You can ask for an index outside of the fields block
527
-
528
- class Advert < ActiveRecord::Base
529
- index :title
530
- end
531
-
532
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
533
- migrate_up(<<~EOS.strip)
534
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
535
- add_index :adverts, [:title], name: :on_title
536
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
537
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
538
- EOS
539
- )
540
-
541
- Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
542
-
543
- # The available options for the index function are `:unique` and `:name`
544
-
545
- class Advert < ActiveRecord::Base
546
- index :title, unique: true, name: 'my_index'
547
- end
548
-
549
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
550
- migrate_up(<<~EOS.strip)
551
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
552
- add_index :adverts, [:title], name: :my_index, unique: true
553
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
554
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
555
- EOS
556
- )
557
-
558
- Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
559
-
560
- # You can create an index on more than one field
561
-
562
- class Advert < ActiveRecord::Base
563
- index [:title, :category_id]
564
- end
565
-
566
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
567
- migrate_up(<<~EOS.strip)
568
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
569
- add_index :adverts, [:title, :category_id], name: :on_title_and_category_id
570
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
571
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
572
- EOS
573
- )
574
-
575
- Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
576
-
577
- # Finally, you can specify that the migration generator should completely ignore an
578
- # index by passing its name to ignore_index in the model.
579
- # This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
580
-
581
- ### Rename a table
582
-
583
- # The migration generator respects the `set_table_name` declaration, although as before,
584
- # we need to explicitly tell the generator that we want a rename rather than a create and a drop.
585
-
586
- class Advert < ActiveRecord::Base
587
- self.table_name = "ads"
588
- fields do
589
- title :string, limit: 250, null: true
590
- body :text, null: true
591
- end
592
- end
593
-
594
- Advert.connection.schema_cache.clear!
595
- Advert.reset_column_information
596
-
597
- expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")).to(
598
- migrate_up(<<~EOS.strip)
599
- rename_table :adverts, :ads
600
- add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
601
- add_column :ads, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
602
- #{if defined?(Mysql2)
603
- "add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
604
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id"
605
- end}
606
- EOS
607
- .and migrate_down(<<~EOS.strip)
608
- #{if defined?(Mysql2)
609
- "remove_foreign_key :adverts, name: :index_adverts_on_c_id\n" +
610
- "remove_foreign_key :adverts, name: :index_adverts_on_category_id"
611
- end}
612
- remove_column :ads, :body
613
- remove_column :ads, :title
614
- rename_table :ads, :adverts
615
- EOS
616
- )
617
-
618
- # Set the table name back to what it should be and confirm we're in sync:
619
-
620
- nuke_model_class(Advert)
621
-
622
- class Advert < ActiveRecord::Base
623
- self.table_name = "adverts"
624
- end
625
-
626
- expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
627
-
628
- ### Rename a table
629
-
630
- # 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.
631
-
632
- nuke_model_class(Advert)
633
-
634
- class Advertisement < ActiveRecord::Base
635
- fields do
636
- title :string, limit: 250, null: true
637
- body :text, null: true
638
- end
639
- end
640
-
641
- expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")).to(
642
- migrate_up(<<~EOS.strip)
643
- rename_table :adverts, :advertisements
644
- add_column :advertisements, :title, :string, limit: 250, null: true#{charset_and_collation}
645
- add_column :advertisements, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
646
- remove_column :advertisements, :name
647
- EOS
648
- .and migrate_down(<<~EOS.strip)
649
- add_column :advertisements, :name, :string, limit: 250, null: true#{charset_and_collation}
650
- remove_column :advertisements, :body
651
- remove_column :advertisements, :title
652
- rename_table :advertisements, :adverts
653
- EOS
654
- )
655
-
656
- ### Drop a table
657
-
658
- nuke_model_class(Advertisement)
659
-
660
- # If you delete a model, the migration generator will create a `drop_table` migration.
661
-
662
- # Dropping tables is where the automatic down-migration really comes in handy:
663
-
664
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
665
- migrate_up(<<~EOS.strip)
666
- drop_table :adverts
667
- EOS
668
- .and migrate_down(<<~EOS.strip)
669
- create_table "adverts"#{table_options}, force: :cascade do |t|
670
- t.string "name", limit: 250#{charset_and_collation}
671
- end
672
- EOS
673
- )
674
-
675
- ## STI
676
-
677
- ### Adding an STI subclass
678
-
679
- # Adding a subclass or two should introduce the 'type' column and no other changes
680
-
681
- class Advert < ActiveRecord::Base
682
- fields do
683
- body :text, null: true
684
- title :string, default: "Untitled", limit: 250, null: true
685
- end
686
- end
687
- up = Generators::DeclareSchema::Migration::Migrator.run.first
688
- ActiveRecord::Migration.class_eval(up)
689
-
690
- class FancyAdvert < Advert
691
- end
692
- class SuperFancyAdvert < FancyAdvert
693
- end
694
-
695
- up, _ = Generators::DeclareSchema::Migration::Migrator.run do |migrations|
696
- expect(migrations).to(
697
- migrate_up(<<~EOS.strip)
698
- add_column :adverts, :type, :string, limit: 250, null: true#{charset_and_collation}
699
- add_index :adverts, [:type], name: :on_type
700
- EOS
701
- .and migrate_down(<<~EOS.strip)
702
- remove_column :adverts, :type
703
- remove_index :adverts, name: :on_type
704
- EOS
705
- )
706
- end
707
-
708
- Advert.field_specs.delete(:type)
709
- nuke_model_class(SuperFancyAdvert)
710
- nuke_model_class(FancyAdvert)
711
- Advert.index_definitions.delete_if { |spec| spec.fields==["type"] }
712
-
713
- ## Coping with multiple changes
714
-
715
- # The migration generator is designed to create complete migrations even if many changes to the models have taken place.
716
-
717
- # First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
718
-
719
- ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
720
- Advert.connection.schema_cache.clear!
721
- Advert.reset_column_information
722
-
723
- expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
724
- to eq(["adverts"])
725
- expect(Advert.columns.map(&:name).sort).to eq(["body", "id", "title"])
726
- expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
727
-
728
-
729
- ### Rename a column and change the default
730
-
731
- Advert.field_specs.clear
732
-
733
- class Advert < ActiveRecord::Base
734
- fields do
735
- name :string, default: "No Name", limit: 250, null: true
736
- body :text, null: true
737
- end
738
- end
739
-
740
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })).to(
741
- migrate_up(<<~EOS.strip)
742
- rename_column :adverts, :title, :name
743
- change_column :adverts, :name, :string, limit: 250, null: true, default: "No Name"#{charset_and_collation}
744
- EOS
745
- .and migrate_down(<<~EOS.strip)
746
- change_column :adverts, :name, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
747
- rename_column :adverts, :name, :title
748
- EOS
749
- )
750
-
751
- ### Rename a table and add a column
752
-
753
- nuke_model_class(Advert)
754
- class Ad < ActiveRecord::Base
755
- fields do
756
- title :string, default: "Untitled", limit: 250
757
- body :text, null: true
758
- created_at :datetime
759
- end
760
- end
761
-
762
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)).to(
763
- migrate_up(<<~EOS.strip)
764
- rename_table :adverts, :ads
765
- add_column :ads, :created_at, :datetime, null: false
766
- change_column :ads, :title, :string, limit: 250, null: false, default: \"Untitled\"#{charset_and_collation}
767
- EOS
768
- )
769
-
770
- class Advert < ActiveRecord::Base
771
- fields do
772
- body :text, null: true
773
- title :string, default: "Untitled", limit: 250, null: true
774
- end
775
- end
776
-
777
- ## Legacy Keys
778
-
779
- # DeclareSchema has some support for legacy keys.
780
-
781
- nuke_model_class(Ad)
782
-
783
- class Advert < ActiveRecord::Base
784
- fields do
785
- body :text, null: true
786
- end
787
- self.primary_key = "advert_id"
788
- end
789
-
790
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })).to(
791
- migrate_up(<<~EOS.strip)
792
- rename_column :adverts, :id, :advert_id
793
- EOS
794
- )
795
-
796
- Advert.connection.schema_cache.clear!
797
- Advert.reset_column_information
798
-
799
- nuke_model_class(Advert)
800
- ActiveRecord::Base.connection.execute("drop table `adverts`;")
801
-
802
- if !defined?(SQLite3) && ActiveSupport::VERSION::MAJOR >= 5
803
- class Advert < ActiveRecord::Base
804
- def self.disable_auto_increment
805
- true
806
- end
807
-
808
- fields do
809
- description :text, limit: 250, null: true
810
- end
811
- end
812
-
813
- up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
814
- expect(migrations).to(
815
- migrate_up(<<~EOS.strip)
816
- create_table :adverts, id: false, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
817
- t.integer :id, limit: 8, auto_increment: false, primary_key: true
818
- t.text :description, limit: 255, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
819
- end
820
- EOS
821
- .and migrate_down("drop_table :adverts")
822
- )
823
- end
824
- end
825
-
826
- ## DSL
827
-
828
- # The DSL allows lambdas and constants
829
-
830
- class User < ActiveRecord::Base
831
- fields do
832
- company :string, limit: 250, ruby_default: -> { "BigCorp" }
833
- end
834
- end
835
- expect(User.field_specs.keys).to eq(['company'])
836
- expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
837
-
838
- ## validates
839
-
840
- # DeclareSchema can accept a validates hash in the field options.
841
-
842
- class Ad < ActiveRecord::Base
843
- class << self
844
- def validates(field_name, options)
845
- end
846
- end
847
- end
848
- expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
849
- class Ad < ActiveRecord::Base
850
- fields do
851
- company :string, limit: 250, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
852
- end
853
- self.primary_key = "advert_id"
854
- end
855
- up, _down = Generators::DeclareSchema::Migration::Migrator.run
856
- ActiveRecord::Migration.class_eval(up)
857
- expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
858
- end
859
-
860
- context 'models with the same parent foreign key relation' do
861
- before do
862
- class Category < ActiveRecord::Base
863
- fields do
864
- name :string, limit: 250, null: true
865
- end
866
- end
867
- class Advertiser < ActiveRecord::Base
868
- fields do
869
- name :string, limit: 250, null: true
870
- end
871
- belongs_to :category, limit: 8
872
- end
873
- class Affiliate < ActiveRecord::Base
874
- fields do
875
- name :string, limit: 250, null: true
876
- end
877
- belongs_to :category, limit: 8
878
- end
879
- end
880
-
881
- it 'will genereate unique constraint names' do
882
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
883
- migrate_up(<<~EOS.strip)
884
- create_table :categories, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
885
- t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
886
- end
887
- create_table :advertisers, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
888
- t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
889
- t.integer :category_id, limit: 8, null: false
890
- end
891
- create_table :affiliates, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
892
- t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
893
- t.integer :category_id, limit: 8, null: false
894
- end
895
- add_index :advertisers, [:category_id], name: :on_category_id
896
- add_index :affiliates, [:category_id], name: :on_category_id
897
- add_foreign_key :advertisers, :categories, column: :category_id, name: :index_advertisers_on_category_id
898
- add_foreign_key :affiliates, :categories, column: :category_id, name: :index_affiliates_on_category_id
899
- EOS
900
- )
901
- migrate
902
-
903
- nuke_model_class(Advertiser)
904
- nuke_model_class(Affiliate)
905
- end
906
- end if !defined?(SQLite3)
907
-
908
- describe 'serialize' do
909
- before do
910
- class Ad < ActiveRecord::Base
911
- @serialize_args = []
912
-
913
- class << self
914
- attr_reader :serialize_args
915
-
916
- def serialize(*args)
917
- @serialize_args << args
918
- end
919
- end
920
- end
921
- end
922
-
923
- describe 'untyped' do
924
- it 'allows serialize: true' do
925
- class Ad < ActiveRecord::Base
926
- fields do
927
- allow_list :text, limit: 0xFFFF, serialize: true
928
- end
929
- end
930
-
931
- expect(Ad.serialize_args).to eq([[:allow_list]])
932
- end
933
-
934
- it 'converts defaults with .to_yaml' do
935
- class Ad < ActiveRecord::Base
936
- fields do
937
- allow_list :string, limit: 250, serialize: true, null: true, default: []
938
- allow_hash :string, limit: 250, serialize: true, null: true, default: {}
939
- allow_string :string, limit: 250, serialize: true, null: true, default: ['abc']
940
- allow_null :string, limit: 250, serialize: true, null: true, default: nil
941
- end
942
- end
943
-
944
- expect(Ad.field_specs['allow_list'].default).to eq("--- []\n")
945
- expect(Ad.field_specs['allow_hash'].default).to eq("--- {}\n")
946
- expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
947
- expect(Ad.field_specs['allow_null'].default).to eq(nil)
948
- end
949
- end
950
-
951
- describe 'Array' do
952
- it 'allows serialize: Array' do
953
- class Ad < ActiveRecord::Base
954
- fields do
955
- allow_list :string, limit: 250, serialize: Array, null: true
956
- end
957
- end
958
-
959
- expect(Ad.serialize_args).to eq([[:allow_list, Array]])
960
- end
961
-
962
- it 'allows Array defaults' do
963
- class Ad < ActiveRecord::Base
964
- fields do
965
- allow_list :string, limit: 250, serialize: Array, null: true, default: [2]
966
- allow_string :string, limit: 250, serialize: Array, null: true, default: ['abc']
967
- allow_empty :string, limit: 250, serialize: Array, null: true, default: []
968
- allow_null :string, limit: 250, serialize: Array, null: true, default: nil
969
- end
970
- end
971
-
972
- expect(Ad.field_specs['allow_list'].default).to eq("---\n- 2\n")
973
- expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
974
- expect(Ad.field_specs['allow_empty'].default).to eq(nil)
975
- expect(Ad.field_specs['allow_null'].default).to eq(nil)
976
- end
977
- end
978
-
979
- describe 'Hash' do
980
- it 'allows serialize: Hash' do
981
- class Ad < ActiveRecord::Base
982
- fields do
983
- allow_list :string, limit: 250, serialize: Hash, null: true
984
- end
985
- end
986
-
987
- expect(Ad.serialize_args).to eq([[:allow_list, Hash]])
988
- end
989
-
990
- it 'allows Hash defaults' do
991
- class Ad < ActiveRecord::Base
992
- fields do
993
- allow_loc :string, limit: 250, serialize: Hash, null: true, default: { 'state' => 'CA' }
994
- allow_hash :string, limit: 250, serialize: Hash, null: true, default: {}
995
- allow_null :string, limit: 250, serialize: Hash, null: true, default: nil
996
- end
997
- end
998
-
999
- expect(Ad.field_specs['allow_loc'].default).to eq("---\nstate: CA\n")
1000
- expect(Ad.field_specs['allow_hash'].default).to eq(nil)
1001
- expect(Ad.field_specs['allow_null'].default).to eq(nil)
1002
- end
1003
- end
1004
-
1005
- describe 'JSON' do
1006
- it 'allows serialize: JSON' do
1007
- class Ad < ActiveRecord::Base
1008
- fields do
1009
- allow_list :string, limit: 250, serialize: JSON
1010
- end
1011
- end
1012
-
1013
- expect(Ad.serialize_args).to eq([[:allow_list, JSON]])
1014
- end
1015
-
1016
- it 'allows JSON defaults' do
1017
- class Ad < ActiveRecord::Base
1018
- fields do
1019
- allow_hash :string, limit: 250, serialize: JSON, null: true, default: { 'state' => 'CA' }
1020
- allow_empty_array :string, limit: 250, serialize: JSON, null: true, default: []
1021
- allow_empty_hash :string, limit: 250, serialize: JSON, null: true, default: {}
1022
- allow_null :string, limit: 250, serialize: JSON, null: true, default: nil
1023
- end
1024
- end
1025
-
1026
- expect(Ad.field_specs['allow_hash'].default).to eq("{\"state\":\"CA\"}")
1027
- expect(Ad.field_specs['allow_empty_array'].default).to eq("[]")
1028
- expect(Ad.field_specs['allow_empty_hash'].default).to eq("{}")
1029
- expect(Ad.field_specs['allow_null'].default).to eq(nil)
1030
- end
1031
- end
1032
-
1033
- class ValueClass
1034
- delegate :present?, :inspect, to: :@value
1035
-
1036
- def initialize(value)
1037
- @value = value
1038
- end
1039
-
1040
- class << self
1041
- def dump(object)
1042
- if object&.present?
1043
- object.inspect
1044
- end
1045
- end
1046
-
1047
- def load(serialized)
1048
- if serialized
1049
- raise 'not used ???'
1050
- end
1051
- end
1052
- end
1053
- end
1054
-
1055
- describe 'custom coder' do
1056
- it 'allows serialize: ValueClass' do
1057
- class Ad < ActiveRecord::Base
1058
- fields do
1059
- allow_list :string, limit: 250, serialize: ValueClass
1060
- end
1061
- end
1062
-
1063
- expect(Ad.serialize_args).to eq([[:allow_list, ValueClass]])
1064
- end
1065
-
1066
- it 'allows ValueClass defaults' do
1067
- class Ad < ActiveRecord::Base
1068
- fields do
1069
- allow_hash :string, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([2])
1070
- allow_empty_array :string, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([])
1071
- allow_null :string, limit: 250, serialize: ValueClass, null: true, default: nil
1072
- end
1073
- end
1074
-
1075
- expect(Ad.field_specs['allow_hash'].default).to eq("[2]")
1076
- expect(Ad.field_specs['allow_empty_array'].default).to eq(nil)
1077
- expect(Ad.field_specs['allow_null'].default).to eq(nil)
1078
- end
1079
- end
1080
-
1081
- it 'disallows serialize: with a non-string column type' do
1082
- expect do
1083
- class Ad < ActiveRecord::Base
1084
- fields do
1085
- allow_list :integer, limit: 8, serialize: true
1086
- end
1087
- end
1088
- end.to raise_exception(ArgumentError, /must be :string or :text/)
1089
- end
1090
- end
1091
-
1092
- context "for Rails #{ActiveSupport::VERSION::MAJOR}" do
1093
- let(:optional_true) { { optional: true } }
1094
- let(:optional_false) { { optional: false } }
1095
- let(:optional_flag) { { false => optional_false, true => optional_true } }
1096
-
1097
- describe 'belongs_to' do
1098
- before do
1099
- unless defined?(AdCategory)
1100
- class AdCategory < ActiveRecord::Base
1101
- fields { }
1102
- end
1103
- end
1104
-
1105
- class Advert < ActiveRecord::Base
1106
- fields do
1107
- name :string, limit: 250, null: true
1108
- category_id :integer, limit: 8
1109
- nullable_category_id :integer, limit: 8, null: true
1110
- end
1111
- end
1112
- up = Generators::DeclareSchema::Migration::Migrator.run.first
1113
- ActiveRecord::Migration.class_eval(up)
1114
- end
1115
-
1116
- it 'passes through optional: when given' do
1117
- class AdvertBelongsTo < ActiveRecord::Base
1118
- self.table_name = 'adverts'
1119
- fields { }
1120
- reset_column_information
1121
- belongs_to :ad_category, optional: true
1122
- end
1123
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
1124
- end
1125
-
1126
- describe 'contradictory settings' do # contradictory settings are ok--for example, during migration
1127
- it 'passes through optional: true, null: false' do
1128
- class AdvertBelongsTo < ActiveRecord::Base
1129
- self.table_name = 'adverts'
1130
- fields { }
1131
- reset_column_information
1132
- belongs_to :ad_category, optional: true, null: false
1133
- end
1134
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
1135
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(false)
1136
- end
1137
-
1138
- it 'passes through optional: false, null: true' do
1139
- class AdvertBelongsTo < ActiveRecord::Base
1140
- self.table_name = 'adverts'
1141
- fields { }
1142
- reset_column_information
1143
- belongs_to :ad_category, optional: false, null: true
1144
- end
1145
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_false)
1146
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(true)
1147
- end
1148
- end
1149
-
1150
- [false, true].each do |nullable|
1151
- context "nullable=#{nullable}" do
1152
- it 'infers optional: from null:' do
1153
- eval <<~EOS
1154
- class AdvertBelongsTo < ActiveRecord::Base
1155
- fields { }
1156
- belongs_to :ad_category, null: #{nullable}
1157
- end
1158
- EOS
1159
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
1160
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
1161
- end
1162
-
1163
- it 'infers null: from optional:' do
1164
- eval <<~EOS
1165
- class AdvertBelongsTo < ActiveRecord::Base
1166
- fields { }
1167
- belongs_to :ad_category, optional: #{nullable}
1168
- end
1169
- EOS
1170
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
1171
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
1172
- end
1173
- end
1174
- end
1175
- end
1176
- end
1177
-
1178
- describe 'migration base class' do
1179
- it 'adapts to Rails 4' do
1180
- class Advert < active_record_base_class.constantize
1181
- fields do
1182
- title :string, limit: 100
1183
- end
1184
- end
1185
-
1186
- generate_migrations '-n', '-m'
1187
-
1188
- migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
1189
- expect(migrations.size).to eq(1), migrations.inspect
1190
-
1191
- migration_content = File.read(migrations.first)
1192
- first_line = migration_content.split("\n").first
1193
- base_class = first_line.split(' < ').last
1194
- expect(base_class).to eq("(ActiveRecord::Migration[4.2])")
1195
- end
1196
- end
1197
-
1198
- context 'Does not generate migrations' do
1199
- it 'for aliased fields bigint -> integer limit 8' do
1200
- class Advert < active_record_base_class.constantize
1201
- fields do
1202
- price :bigint
1203
- end
1204
- end
1205
-
1206
- generate_migrations '-n', '-m'
1207
-
1208
- migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
1209
- expect(migrations.size).to eq(1), migrations.inspect
1210
-
1211
- class Advert < active_record_base_class.constantize
1212
- fields do
1213
- price :integer, limit: 8
1214
- end
1215
- end
1216
-
1217
- expect { generate_migrations '-n', '-g' }.to output("Database and models match -- nothing to change\n").to_stdout
1218
- end
1219
- end
1220
- end
1221
-
1222
57
  context 'Using declare_schema' do
1223
58
  # DeclareSchema - Migration Generator
1224
59
  it 'generates migrations' do
@@ -1758,7 +593,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
1758
593
  Advert.connection.schema_cache.clear!
1759
594
  Advert.reset_column_information
1760
595
 
1761
- expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")).to(
596
+ expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: "ads")).to(
1762
597
  migrate_up(<<~EOS.strip)
1763
598
  rename_table :adverts, :ads
1764
599
  add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
@@ -1802,7 +637,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
1802
637
  end
1803
638
  end
1804
639
 
1805
- expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")).to(
640
+ expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: "advertisements")).to(
1806
641
  migrate_up(<<~EOS.strip)
1807
642
  rename_table :adverts, :advertisements
1808
643
  add_column :advertisements, :title, :string, limit: 250, null: true#{charset_and_collation}
@@ -1999,19 +834,19 @@ RSpec.describe 'DeclareSchema Migration Generator' do
1999
834
  context 'models with the same parent foreign key relation' do
2000
835
  before do
2001
836
  class Category < ActiveRecord::Base
2002
- fields do
2003
- name :string, limit: 250, null: true
837
+ declare_schema do
838
+ string :name, limit: 250, null: true
2004
839
  end
2005
840
  end
2006
841
  class Advertiser < ActiveRecord::Base
2007
- fields do
2008
- name :string, limit: 250, null: true
842
+ declare_schema do
843
+ string :name, limit: 250, null: true
2009
844
  end
2010
845
  belongs_to :category, limit: 8
2011
846
  end
2012
847
  class Affiliate < ActiveRecord::Base
2013
- fields do
2014
- name :string, limit: 250, null: true
848
+ declare_schema do
849
+ string :name, limit: 250, null: true
2015
850
  end
2016
851
  belongs_to :category, limit: 8
2017
852
  end
@@ -2339,9 +1174,9 @@ RSpec.describe 'DeclareSchema Migration Generator' do
2339
1174
  end
2340
1175
  class Fk < ActiveRecord::Base
2341
1176
  declare_schema { }
2342
- belongs_to :id_default, ({})
2343
- belongs_to :id4, ({})
2344
- belongs_to :id8, ({})
1177
+ belongs_to :id_default
1178
+ belongs_to :id4
1179
+ belongs_to :id8
2345
1180
  end
2346
1181
  end
2347
1182