declare_schema 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +37 -0
- data/CHANGELOG.md +28 -4
- data/Gemfile +0 -2
- data/Gemfile.lock +1 -4
- data/README.md +59 -2
- data/Rakefile +13 -20
- data/gemfiles/rails_4.gemfile +4 -7
- data/gemfiles/rails_5.gemfile +4 -7
- data/gemfiles/rails_6.gemfile +4 -7
- data/lib/declare_schema/model.rb +0 -1
- data/lib/declare_schema/model/field_spec.rb +4 -14
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +20 -13
- data/lib/generators/declare_schema/migration/migrator.rb +58 -38
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
- data/lib/generators/declare_schema/support/eval_template.rb +12 -3
- data/lib/generators/declare_schema/support/model.rb +77 -2
- data/spec/lib/declare_schema/api_spec.rb +125 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
- data/spec/lib/declare_schema/generator_spec.rb +57 -0
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
- data/spec/lib/declare_schema/migration_generator_spec.rb +686 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +29 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +42 -0
- data/spec/spec_helper.rb +26 -0
- metadata +9 -12
- data/.jenkins/Jenkinsfile +0 -72
- data/.jenkins/ruby_build_pod.yml +0 -19
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
- data/test/api.rdoctest +0 -136
- data/test/doc-only.rdoctest +0 -76
- data/test/generators.rdoctest +0 -60
- data/test/interactive_primary_key.rdoctest +0 -56
- data/test/migration_generator.rdoctest +0 -846
- data/test/migration_generator_comments.rdoctestDISABLED +0 -74
- data/test/prepare_testapp.rb +0 -15
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe 'DeclareSchema Migration Generator interactive primary key' do
|
4
|
+
before do
|
5
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "allows alternate primary keys" do
|
9
|
+
class Foo < ActiveRecord::Base
|
10
|
+
fields do
|
11
|
+
end
|
12
|
+
self.primary_key = "foo_id"
|
13
|
+
end
|
14
|
+
|
15
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
16
|
+
expect(Foo.primary_key).to eq('foo_id')
|
17
|
+
|
18
|
+
### migrate from
|
19
|
+
# rename from custom primary_key
|
20
|
+
class Foo < ActiveRecord::Base
|
21
|
+
fields do
|
22
|
+
end
|
23
|
+
self.primary_key = "id"
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
|
27
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
28
|
+
expect(Foo.primary_key).to eq('id')
|
29
|
+
|
30
|
+
nuke_model_class(Foo)
|
31
|
+
|
32
|
+
### migrate to
|
33
|
+
|
34
|
+
# rename to custom primary_key
|
35
|
+
class Foo < ActiveRecord::Base
|
36
|
+
fields do
|
37
|
+
end
|
38
|
+
self.primary_key = "foo_id"
|
39
|
+
end
|
40
|
+
|
41
|
+
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
42
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
43
|
+
expect(Foo.primary_key).to eq('foo_id')
|
44
|
+
|
45
|
+
### ensure it doesn't cause further migrations
|
46
|
+
|
47
|
+
# check no further migrations
|
48
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
49
|
+
expect(up).to eq("")
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,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, limit: 4294967295
|
200
|
+
add_column :adverts, :description, :text, null: false, limit: 255
|
201
|
+
EOS
|
202
|
+
|
203
|
+
Advert.field_specs.delete :notes
|
204
|
+
|
205
|
+
# Limits that are too high for MySQL will raise an exception.
|
206
|
+
|
207
|
+
::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
|
208
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
209
|
+
expect do
|
210
|
+
class Advert < ActiveRecord::Base
|
211
|
+
fields do
|
212
|
+
notes :text
|
213
|
+
description :text, limit: 0x1_0000_0000
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
|
217
|
+
|
218
|
+
Advert.field_specs.delete :notes
|
219
|
+
|
220
|
+
# And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
|
221
|
+
|
222
|
+
# To start, we'll set the database schema for `description` to match the above limit of 255.
|
223
|
+
|
224
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
225
|
+
Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
|
226
|
+
Advert.connection.schema_cache.clear!
|
227
|
+
Advert.reset_column_information
|
228
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
229
|
+
to eq(["adverts"])
|
230
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "body", "title", "description"])
|
231
|
+
|
232
|
+
# Now migrate to an unstated text limit:
|
233
|
+
|
234
|
+
class Advert < ActiveRecord::Base
|
235
|
+
fields do
|
236
|
+
description :text
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
241
|
+
expect(up).to eq("change_column :adverts, :description, :text, limit: 4294967295, null: false")
|
242
|
+
expect(down).to eq("change_column :adverts, :description, :text")
|
243
|
+
|
244
|
+
# TODO TECH-4814: The above test should have this output:
|
245
|
+
# TODO => "change_column :adverts, :description, :text, limit: 255
|
246
|
+
|
247
|
+
# And migrate to a stated text limit that is the same as the unstated one:
|
248
|
+
|
249
|
+
class Advert < ActiveRecord::Base
|
250
|
+
fields do
|
251
|
+
description :text, limit: 0xffffffff
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
256
|
+
expect(up).to eq("change_column :adverts, :description, :text, limit: 4294967295, null: false")
|
257
|
+
expect(down).to eq("change_column :adverts, :description, :text")
|
258
|
+
::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, false)
|
259
|
+
|
260
|
+
Advert.field_specs.clear
|
261
|
+
Advert.connection.schema_cache.clear!
|
262
|
+
Advert.reset_column_information
|
263
|
+
class Advert < ActiveRecord::Base
|
264
|
+
fields do
|
265
|
+
name :string, limit: 255, null: true
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
up, 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
|
+
|