declare_schema 0.8.0.pre.5 → 0.10.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/.github/workflows/declare_schema_build.yml +1 -1
- data/CHANGELOG.md +21 -1
- data/Gemfile.lock +1 -1
- data/README.md +91 -13
- data/lib/declare_schema.rb +46 -0
- data/lib/declare_schema/dsl.rb +39 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +23 -4
- data/lib/declare_schema/model.rb +51 -59
- data/lib/declare_schema/model/column.rb +2 -0
- data/lib/declare_schema/model/field_spec.rb +11 -8
- data/lib/declare_schema/model/habtm_model_shim.rb +1 -1
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +22 -33
- data/lib/generators/declare_schema/support/model.rb +4 -4
- data/spec/lib/declare_schema/api_spec.rb +7 -7
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +41 -15
- data/spec/lib/declare_schema/field_spec_spec.rb +73 -22
- data/spec/lib/declare_schema/generator_spec.rb +3 -3
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +78 -26
- data/spec/lib/declare_schema/migration_generator_spec.rb +1989 -815
- data/spec/lib/declare_schema/model/column_spec.rb +47 -17
- data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +146 -57
- data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +3 -3
- data/spec/lib/declare_schema/model/index_definition_spec.rb +188 -77
- data/spec/lib/declare_schema/model/table_options_definition_spec.rb +75 -11
- data/spec/lib/declare_schema_spec.rb +101 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +12 -2
- metadata +7 -6
- data/test_responses.txt +0 -2
@@ -11,9 +11,9 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
11
11
|
expect_model_definition_to_eq('alpha/beta', <<~EOS)
|
12
12
|
class Alpha::Beta < #{active_record_base_class}
|
13
13
|
|
14
|
-
|
15
|
-
one
|
16
|
-
|
14
|
+
declare_schema do
|
15
|
+
string :one, limit: 255
|
16
|
+
integer :two
|
17
17
|
end
|
18
18
|
|
19
19
|
end
|
@@ -11,49 +11,101 @@ RSpec.describe 'DeclareSchema Migration Generator interactive primary key' do
|
|
11
11
|
load File.expand_path('prepare_testapp.rb', __dir__)
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
context 'Using fields' do
|
15
|
+
it "allows alternate primary keys" do
|
16
|
+
class Foo < ActiveRecord::Base
|
17
|
+
fields do
|
18
|
+
end
|
19
|
+
self.primary_key = "foo_id"
|
17
20
|
end
|
18
|
-
self.primary_key = "foo_id"
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
generate_migrations '-n', '-m'
|
23
|
+
expect(Foo._defined_primary_key).to eq('foo_id')
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
### migrate from
|
26
|
+
# rename from custom primary_key
|
27
|
+
class Foo < ActiveRecord::Base
|
28
|
+
fields do
|
29
|
+
end
|
30
|
+
self.primary_key = "id"
|
28
31
|
end
|
29
|
-
self.primary_key = "id"
|
30
|
-
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
allow_any_instance_of(DeclareSchema::Support::ThorShell).to receive(:ask).with(/one of the rename choices or press enter to keep/) { 'id' }
|
34
|
+
generate_migrations '-n', '-m'
|
35
|
+
expect(Foo._defined_primary_key).to eq('id')
|
36
|
+
|
37
|
+
nuke_model_class(Foo)
|
38
|
+
|
39
|
+
### migrate to
|
40
|
+
|
41
|
+
if Rails::VERSION::MAJOR >= 5 && !defined?(Mysql2) # TODO TECH-4814 Put this test back for Mysql2
|
42
|
+
# replace custom primary_key
|
43
|
+
class Foo < ActiveRecord::Base
|
44
|
+
fields do
|
45
|
+
end
|
46
|
+
self.primary_key = "foo_id"
|
47
|
+
end
|
48
|
+
|
49
|
+
allow_any_instance_of(DeclareSchema::Support::ThorShell).to receive(:ask).with(/one of the rename choices or press enter to keep/) { 'drop id' }
|
50
|
+
generate_migrations '-n', '-m'
|
51
|
+
expect(Foo._defined_primary_key).to eq('foo_id')
|
35
52
|
|
36
|
-
|
53
|
+
### ensure it doesn't cause further migrations
|
37
54
|
|
38
|
-
|
55
|
+
# check no further migrations
|
56
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
57
|
+
expect(up).to eq("")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
39
61
|
|
40
|
-
|
41
|
-
|
62
|
+
context 'Using declare_schema' do
|
63
|
+
it "allows alternate primary keys" do
|
42
64
|
class Foo < ActiveRecord::Base
|
43
|
-
|
65
|
+
declare_schema do
|
44
66
|
end
|
45
67
|
self.primary_key = "foo_id"
|
46
68
|
end
|
47
69
|
|
48
|
-
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
49
70
|
generate_migrations '-n', '-m'
|
50
71
|
expect(Foo.primary_key).to eq('foo_id')
|
51
72
|
|
52
|
-
###
|
73
|
+
### migrate from
|
74
|
+
# rename from custom primary_key
|
75
|
+
class Foo < ActiveRecord::Base
|
76
|
+
declare_schema do
|
77
|
+
end
|
78
|
+
self.primary_key = "id"
|
79
|
+
end
|
80
|
+
|
81
|
+
puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
|
82
|
+
allow_any_instance_of(DeclareSchema::Support::ThorShell).to receive(:ask).with(/one of the rename choices or press enter to keep/) { 'id' }
|
83
|
+
generate_migrations '-n', '-m'
|
84
|
+
expect(Foo.primary_key).to eq('id')
|
85
|
+
|
86
|
+
nuke_model_class(Foo)
|
87
|
+
|
88
|
+
### migrate to
|
89
|
+
|
90
|
+
if Rails::VERSION::MAJOR >= 5 && !defined?(Mysql2) # TODO TECH-4814 Put this test back for Mysql2
|
91
|
+
# replace custom primary_key
|
92
|
+
class Foo < ActiveRecord::Base
|
93
|
+
declare_schema do
|
94
|
+
end
|
95
|
+
self.primary_key = "foo_id"
|
96
|
+
end
|
97
|
+
|
98
|
+
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
99
|
+
allow_any_instance_of(DeclareSchema::Support::ThorShell).to receive(:ask).with(/one of the rename choices or press enter to keep/) { 'drop id' }
|
100
|
+
generate_migrations '-n', '-m'
|
101
|
+
expect(Foo.primary_key).to eq('foo_id')
|
53
102
|
|
54
|
-
|
55
|
-
|
56
|
-
|
103
|
+
### ensure it doesn't cause further migrations
|
104
|
+
|
105
|
+
# check no further migrations
|
106
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
107
|
+
expect(up).to eq("")
|
108
|
+
end
|
57
109
|
end
|
58
110
|
end
|
59
111
|
end
|
@@ -55,1172 +55,2346 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
58
|
+
context 'Using fields' do
|
59
|
+
# DeclareSchema - Migration Generator
|
60
|
+
it 'generates migrations' do
|
61
|
+
## The migration generator -- introduction
|
61
62
|
|
62
|
-
|
63
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
|
63
64
|
|
64
|
-
|
65
|
-
|
65
|
+
class Advert < ActiveRecord::Base
|
66
|
+
end
|
66
67
|
|
67
|
-
|
68
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
|
68
69
|
|
69
|
-
|
70
|
+
Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
|
70
71
|
|
71
|
-
|
72
|
-
|
72
|
+
Advert.connection.schema_cache.clear!
|
73
|
+
Advert.reset_column_information
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
75
|
+
class Advert < ActiveRecord::Base
|
76
|
+
fields do
|
77
|
+
name :string, limit: 250, null: true
|
78
|
+
end
|
77
79
|
end
|
78
|
-
end
|
79
80
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
81
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
|
82
|
+
expect(migrations).to(
|
83
|
+
migrate_up(<<~EOS.strip)
|
84
|
+
create_table :adverts, id: :bigint do |t|
|
85
|
+
t.string :name, limit: 250, null: true#{charset_and_collation}
|
86
|
+
end#{charset_alter_table}
|
87
|
+
EOS
|
88
|
+
.and migrate_down("drop_table :adverts")
|
89
|
+
)
|
90
|
+
end
|
90
91
|
|
91
|
-
|
92
|
-
|
92
|
+
ActiveRecord::Migration.class_eval(up)
|
93
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "name"])
|
93
94
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
95
|
+
if Rails::VERSION::MAJOR < 5
|
96
|
+
# Rails 4 drivers don't always create PK properly. Fix that by dropping and recreating.
|
97
|
+
ActiveRecord::Base.connection.execute("drop table adverts")
|
98
|
+
if defined?(Mysql2)
|
99
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE adverts (id integer PRIMARY KEY AUTO_INCREMENT NOT NULL, name varchar(250)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin")
|
100
|
+
else
|
101
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE adverts (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, name varchar(250))")
|
102
|
+
end
|
101
103
|
end
|
102
|
-
end
|
103
104
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
105
|
+
class Advert < ActiveRecord::Base
|
106
|
+
fields do
|
107
|
+
name :string, limit: 250, null: true
|
108
|
+
body :text, null: true
|
109
|
+
published_at :datetime, null: true
|
110
|
+
end
|
109
111
|
end
|
110
|
-
end
|
111
112
|
|
112
|
-
|
113
|
-
|
113
|
+
Advert.connection.schema_cache.clear!
|
114
|
+
Advert.reset_column_information
|
114
115
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
116
|
+
expect(migrate).to(
|
117
|
+
migrate_up(<<~EOS.strip)
|
118
|
+
add_column :adverts, :body, :text#{text_limit}, null: true#{charset_and_collation}
|
119
|
+
add_column :adverts, :published_at, :datetime, null: true
|
120
|
+
EOS
|
121
|
+
.and migrate_down(<<~EOS.strip)
|
122
|
+
remove_column :adverts, :body
|
123
|
+
remove_column :adverts, :published_at
|
124
|
+
EOS
|
125
|
+
)
|
125
126
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
127
|
+
Advert.field_specs.clear # not normally needed
|
128
|
+
class Advert < ActiveRecord::Base
|
129
|
+
fields do
|
130
|
+
name :string, limit: 250, null: true
|
131
|
+
body :text, null: true
|
132
|
+
end
|
131
133
|
end
|
132
|
-
end
|
133
134
|
|
134
|
-
|
135
|
-
|
136
|
-
|
135
|
+
expect(migrate).to(
|
136
|
+
migrate_up("remove_column :adverts, :published_at").and(
|
137
|
+
migrate_down("add_column :adverts, :published_at, :datetime#{datetime_precision}, null: true")
|
138
|
+
)
|
137
139
|
)
|
138
|
-
)
|
139
140
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
141
|
+
nuke_model_class(Advert)
|
142
|
+
class Advert < ActiveRecord::Base
|
143
|
+
fields do
|
144
|
+
title :string, limit: 250, null: true
|
145
|
+
body :text, null: true
|
146
|
+
end
|
145
147
|
end
|
146
|
-
end
|
147
148
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
149
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
150
|
+
migrate_up(<<~EOS.strip)
|
151
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
152
|
+
remove_column :adverts, :name
|
153
|
+
EOS
|
154
|
+
.and migrate_down(<<~EOS.strip)
|
155
|
+
remove_column :adverts, :title
|
156
|
+
add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
|
157
|
+
EOS
|
158
|
+
)
|
158
159
|
|
159
|
-
|
160
|
-
|
161
|
-
|
160
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })).to(
|
161
|
+
migrate_up("rename_column :adverts, :name, :title").and(
|
162
|
+
migrate_down("rename_column :adverts, :title, :name")
|
163
|
+
)
|
162
164
|
)
|
163
|
-
)
|
164
165
|
|
165
|
-
|
166
|
+
migrate
|
166
167
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
168
|
+
class Advert < ActiveRecord::Base
|
169
|
+
fields do
|
170
|
+
title :text, null: true
|
171
|
+
body :text, null: true
|
172
|
+
end
|
171
173
|
end
|
172
|
-
end
|
173
174
|
|
174
|
-
|
175
|
-
|
176
|
-
|
175
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
176
|
+
migrate_up("change_column :adverts, :title, :text#{text_limit}, null: true#{charset_and_collation}").and(
|
177
|
+
migrate_down("change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}")
|
178
|
+
)
|
177
179
|
)
|
178
|
-
)
|
179
180
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
181
|
+
class Advert < ActiveRecord::Base
|
182
|
+
fields do
|
183
|
+
title :string, default: "Untitled", limit: 250, null: true
|
184
|
+
body :text, null: true
|
185
|
+
end
|
184
186
|
end
|
185
|
-
end
|
186
187
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
188
|
+
expect(migrate).to(
|
189
|
+
migrate_up(<<~EOS.strip)
|
190
|
+
change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
|
191
|
+
EOS
|
192
|
+
.and migrate_down(<<~EOS.strip)
|
193
|
+
change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
194
|
+
EOS
|
195
|
+
)
|
195
196
|
|
196
|
-
|
197
|
+
### Limits
|
197
198
|
|
198
|
-
|
199
|
-
|
200
|
-
|
199
|
+
class Advert < ActiveRecord::Base
|
200
|
+
fields do
|
201
|
+
price :integer, null: true, limit: 2
|
202
|
+
end
|
201
203
|
end
|
202
|
-
end
|
203
204
|
|
204
|
-
|
205
|
-
|
206
|
-
end
|
207
|
-
|
208
|
-
# Now run the migration, then change the limit:
|
209
|
-
|
210
|
-
ActiveRecord::Migration.class_eval(up)
|
211
|
-
class Advert < ActiveRecord::Base
|
212
|
-
fields do
|
213
|
-
price :integer, null: true, limit: 3
|
205
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
|
206
|
+
expect(migrations).to migrate_up("add_column :adverts, :price, :integer, limit: 2, null: true")
|
214
207
|
end
|
215
|
-
end
|
216
208
|
|
217
|
-
|
218
|
-
migrate_up(<<~EOS.strip)
|
219
|
-
change_column :adverts, :price, :integer, limit: 3, null: true
|
220
|
-
EOS
|
221
|
-
.and migrate_down(<<~EOS.strip)
|
222
|
-
change_column :adverts, :price, :integer, limit: 2, null: true
|
223
|
-
EOS
|
224
|
-
)
|
209
|
+
# Now run the migration, then change the limit:
|
225
210
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
211
|
+
ActiveRecord::Migration.class_eval(up)
|
212
|
+
class Advert < ActiveRecord::Base
|
213
|
+
fields do
|
214
|
+
price :integer, null: true, limit: 3
|
215
|
+
end
|
230
216
|
end
|
231
|
-
end
|
232
217
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
218
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
219
|
+
migrate_up(<<~EOS.strip)
|
220
|
+
change_column :adverts, :price, :integer, limit: 3, null: true
|
221
|
+
EOS
|
222
|
+
.and migrate_down(<<~EOS.strip)
|
223
|
+
change_column :adverts, :price, :integer, limit: 2, null: true
|
224
|
+
EOS
|
225
|
+
)
|
241
226
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
227
|
+
ActiveRecord::Migration.class_eval("remove_column :adverts, :price")
|
228
|
+
class Advert < ActiveRecord::Base
|
229
|
+
fields do
|
230
|
+
price :decimal, precision: 4, scale: 1, null: true
|
231
|
+
end
|
246
232
|
end
|
247
|
-
end
|
248
|
-
|
249
|
-
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
250
|
-
migrate_up(<<~EOS.strip)
|
251
|
-
add_column :adverts, :price, :decimal, precision: 4, scale: 1, null: true
|
252
|
-
add_column :adverts, :notes, :text#{text_limit}, null: false#{charset_and_collation}
|
253
|
-
add_column :adverts, :description, :text#{', limit: 65535' if defined?(Mysql2)}, null: false#{charset_and_collation}
|
254
|
-
EOS
|
255
|
-
)
|
256
233
|
|
257
|
-
|
258
|
-
|
259
|
-
|
234
|
+
# Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
|
235
|
+
# allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
|
236
|
+
# If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
|
237
|
+
# that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
|
260
238
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
239
|
+
if defined?(SQLite3)
|
240
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_falsey
|
241
|
+
end
|
265
242
|
|
266
243
|
class Advert < ActiveRecord::Base
|
267
244
|
fields do
|
268
245
|
notes :text
|
269
|
-
description :text, limit:
|
246
|
+
description :text, limit: 30000
|
270
247
|
end
|
271
248
|
end
|
272
249
|
|
273
250
|
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
274
251
|
migrate_up(<<~EOS.strip)
|
275
|
-
add_column :adverts, :
|
276
|
-
add_column :adverts, :
|
252
|
+
add_column :adverts, :price, :decimal, precision: 4, scale: 1, null: true
|
253
|
+
add_column :adverts, :notes, :text#{text_limit}, null: false#{charset_and_collation}
|
254
|
+
add_column :adverts, :description, :text#{', limit: 65535' if defined?(Mysql2)}, null: false#{charset_and_collation}
|
277
255
|
EOS
|
278
256
|
)
|
279
257
|
|
258
|
+
Advert.field_specs.delete :price
|
280
259
|
Advert.field_specs.delete :notes
|
260
|
+
Advert.field_specs.delete :description
|
261
|
+
|
262
|
+
# In MySQL, limits are applied, rounded up:
|
281
263
|
|
282
|
-
|
264
|
+
if defined?(Mysql2)
|
265
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
283
266
|
|
284
|
-
expect do
|
285
267
|
class Advert < ActiveRecord::Base
|
286
268
|
fields do
|
287
269
|
notes :text
|
288
|
-
description :text, limit:
|
270
|
+
description :text, limit: 250
|
289
271
|
end
|
290
272
|
end
|
291
|
-
end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
|
292
273
|
|
293
|
-
|
274
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
275
|
+
migrate_up(<<~EOS.strip)
|
276
|
+
add_column :adverts, :notes, :text, limit: 4294967295, null: false#{charset_and_collation}
|
277
|
+
add_column :adverts, :description, :text, limit: 255, null: false#{charset_and_collation}
|
278
|
+
EOS
|
279
|
+
)
|
280
|
+
|
281
|
+
Advert.field_specs.delete :notes
|
282
|
+
|
283
|
+
# Limits that are too high for MySQL will raise an exception.
|
284
|
+
|
285
|
+
expect do
|
286
|
+
class Advert < ActiveRecord::Base
|
287
|
+
fields do
|
288
|
+
notes :text
|
289
|
+
description :text, limit: 0x1_0000_0000
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
|
293
|
+
|
294
|
+
Advert.field_specs.delete :notes
|
295
|
+
|
296
|
+
# And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
|
297
|
+
|
298
|
+
# To start, we'll set the database schema for `description` to match the above limit of 250.
|
299
|
+
|
300
|
+
Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
|
301
|
+
Advert.connection.schema_cache.clear!
|
302
|
+
Advert.reset_column_information
|
303
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
304
|
+
to eq(["adverts"])
|
305
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "body", "title", "description"])
|
306
|
+
|
307
|
+
# Now migrate to an unstated text limit:
|
308
|
+
|
309
|
+
class Advert < ActiveRecord::Base
|
310
|
+
fields do
|
311
|
+
description :text
|
312
|
+
end
|
313
|
+
end
|
294
314
|
|
295
|
-
|
315
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
316
|
+
migrate_up(<<~EOS.strip)
|
317
|
+
change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
|
318
|
+
EOS
|
319
|
+
.and migrate_down(<<~EOS.strip)
|
320
|
+
change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
321
|
+
EOS
|
322
|
+
)
|
296
323
|
|
297
|
-
|
324
|
+
# And migrate to a stated text limit that is the same as the unstated one:
|
298
325
|
|
299
|
-
|
326
|
+
class Advert < ActiveRecord::Base
|
327
|
+
fields do
|
328
|
+
description :text, limit: 0xffffffff
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
333
|
+
migrate_up(<<~EOS.strip)
|
334
|
+
change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
|
335
|
+
EOS
|
336
|
+
.and migrate_down(<<~EOS.strip)
|
337
|
+
change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
338
|
+
EOS
|
339
|
+
)
|
340
|
+
end
|
341
|
+
|
342
|
+
Advert.field_specs.clear
|
300
343
|
Advert.connection.schema_cache.clear!
|
301
344
|
Advert.reset_column_information
|
302
|
-
|
303
|
-
|
304
|
-
|
345
|
+
class Advert < ActiveRecord::Base
|
346
|
+
fields do
|
347
|
+
name :string, limit: 250, null: true
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
352
|
+
ActiveRecord::Migration.class_eval up
|
353
|
+
|
354
|
+
Advert.connection.schema_cache.clear!
|
355
|
+
Advert.reset_column_information
|
356
|
+
|
357
|
+
### Foreign Keys
|
358
|
+
|
359
|
+
# DeclareSchema extends the `belongs_to` macro so that it also declares the
|
360
|
+
# foreign-key field. It also generates an index on the field.
|
361
|
+
|
362
|
+
class Category < ActiveRecord::Base; end
|
363
|
+
class Advert < ActiveRecord::Base
|
364
|
+
fields do
|
365
|
+
name :string, limit: 250, null: true
|
366
|
+
end
|
367
|
+
belongs_to :category
|
368
|
+
end
|
369
|
+
|
370
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
371
|
+
migrate_up(<<~EOS.strip)
|
372
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
373
|
+
|
374
|
+
add_index :adverts, [:category_id], name: 'on_category_id'
|
375
|
+
|
376
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" if defined?(Mysql2)}
|
377
|
+
EOS
|
378
|
+
.and migrate_down(<<~EOS.strip)
|
379
|
+
remove_column :adverts, :category_id
|
380
|
+
|
381
|
+
remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid
|
382
|
+
|
383
|
+
#{"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" if defined?(Mysql2)}
|
384
|
+
EOS
|
385
|
+
)
|
386
|
+
|
387
|
+
Advert.field_specs.delete(:category_id)
|
388
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
|
389
|
+
|
390
|
+
# If you specify a custom foreign key, the migration generator observes that:
|
391
|
+
|
392
|
+
class Category < ActiveRecord::Base; end
|
393
|
+
class Advert < ActiveRecord::Base
|
394
|
+
fields { }
|
395
|
+
belongs_to :category, foreign_key: "c_id", class_name: 'Category'
|
396
|
+
end
|
397
|
+
|
398
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
399
|
+
migrate_up(<<~EOS.strip)
|
400
|
+
add_column :adverts, :c_id, :integer, limit: 8, null: false
|
401
|
+
|
402
|
+
add_index :adverts, [:c_id], name: 'on_c_id'
|
403
|
+
|
404
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
405
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
406
|
+
EOS
|
407
|
+
)
|
408
|
+
|
409
|
+
Advert.field_specs.delete(:c_id)
|
410
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
|
305
411
|
|
306
|
-
#
|
412
|
+
# You can avoid generating the index by specifying `index: false`
|
413
|
+
|
414
|
+
class Category < ActiveRecord::Base; end
|
415
|
+
class Advert < ActiveRecord::Base
|
416
|
+
fields { }
|
417
|
+
belongs_to :category, index: false
|
418
|
+
end
|
419
|
+
|
420
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
421
|
+
migrate_up(<<~EOS.strip)
|
422
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
423
|
+
|
424
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
425
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
426
|
+
EOS
|
427
|
+
)
|
428
|
+
|
429
|
+
Advert.field_specs.delete(:category_id)
|
430
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
431
|
+
|
432
|
+
# You can specify the index name with :index
|
433
|
+
|
434
|
+
class Category < ActiveRecord::Base; end
|
435
|
+
class Advert < ActiveRecord::Base
|
436
|
+
fields { }
|
437
|
+
belongs_to :category, index: 'my_index'
|
438
|
+
end
|
439
|
+
|
440
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
441
|
+
migrate_up(<<~EOS.strip)
|
442
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
443
|
+
|
444
|
+
add_index :adverts, [:category_id], name: 'my_index'
|
445
|
+
|
446
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
447
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
448
|
+
EOS
|
449
|
+
)
|
450
|
+
|
451
|
+
Advert.field_specs.delete(:category_id)
|
452
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
453
|
+
|
454
|
+
### Timestamps and Optimimistic Locking
|
455
|
+
|
456
|
+
# `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
|
457
|
+
# Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
|
307
458
|
|
308
459
|
class Advert < ActiveRecord::Base
|
309
460
|
fields do
|
310
|
-
|
461
|
+
timestamps
|
462
|
+
optimistic_lock
|
311
463
|
end
|
312
464
|
end
|
313
465
|
|
314
466
|
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
315
467
|
migrate_up(<<~EOS.strip)
|
316
|
-
|
468
|
+
add_column :adverts, :created_at, :datetime, null: true
|
469
|
+
add_column :adverts, :updated_at, :datetime, null: true
|
470
|
+
add_column :adverts, :lock_version, :integer#{lock_version_limit}, null: false, default: 1
|
471
|
+
|
472
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
473
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
317
474
|
EOS
|
318
475
|
.and migrate_down(<<~EOS.strip)
|
319
|
-
|
476
|
+
remove_column :adverts, :created_at
|
477
|
+
remove_column :adverts, :updated_at
|
478
|
+
remove_column :adverts, :lock_version
|
479
|
+
|
480
|
+
#{"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" +
|
481
|
+
"remove_foreign_key(\"adverts\", name: \"on_c_id\")" if defined?(Mysql2)}
|
482
|
+
EOS
|
483
|
+
)
|
484
|
+
|
485
|
+
Advert.field_specs.delete(:updated_at)
|
486
|
+
Advert.field_specs.delete(:created_at)
|
487
|
+
Advert.field_specs.delete(:lock_version)
|
488
|
+
|
489
|
+
### Indices
|
490
|
+
|
491
|
+
# You can add an index to a field definition
|
492
|
+
|
493
|
+
class Advert < ActiveRecord::Base
|
494
|
+
fields do
|
495
|
+
title :string, index: true, limit: 250, null: true
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
500
|
+
migrate_up(<<~EOS.strip)
|
501
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
502
|
+
|
503
|
+
add_index :adverts, [:title], name: 'on_title'
|
504
|
+
|
505
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
506
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
507
|
+
EOS
|
508
|
+
)
|
509
|
+
|
510
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
511
|
+
|
512
|
+
# You can ask for a unique index
|
513
|
+
|
514
|
+
class Advert < ActiveRecord::Base
|
515
|
+
fields do
|
516
|
+
title :string, index: true, unique: true, null: true, limit: 250
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
521
|
+
migrate_up(<<~EOS.strip)
|
522
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
523
|
+
|
524
|
+
add_index :adverts, [:title], unique: true, name: 'on_title'
|
525
|
+
|
526
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
527
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
528
|
+
EOS
|
529
|
+
)
|
530
|
+
|
531
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
532
|
+
|
533
|
+
# You can specify the name for the index
|
534
|
+
|
535
|
+
class Advert < ActiveRecord::Base
|
536
|
+
fields do
|
537
|
+
title :string, index: 'my_index', limit: 250, null: true
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
542
|
+
migrate_up(<<~EOS.strip)
|
543
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
544
|
+
|
545
|
+
add_index :adverts, [:title], name: 'my_index'
|
546
|
+
|
547
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
548
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
549
|
+
EOS
|
550
|
+
)
|
551
|
+
|
552
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
553
|
+
|
554
|
+
# You can ask for an index outside of the fields block
|
555
|
+
|
556
|
+
class Advert < ActiveRecord::Base
|
557
|
+
index :title
|
558
|
+
end
|
559
|
+
|
560
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
561
|
+
migrate_up(<<~EOS.strip)
|
562
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
563
|
+
|
564
|
+
add_index :adverts, [:title], name: 'on_title'
|
565
|
+
|
566
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
567
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
320
568
|
EOS
|
321
569
|
)
|
322
570
|
|
323
|
-
|
571
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
572
|
+
|
573
|
+
# The available options for the index function are `:unique` and `:name`
|
574
|
+
|
575
|
+
class Advert < ActiveRecord::Base
|
576
|
+
index :title, unique: true, name: 'my_index'
|
577
|
+
end
|
578
|
+
|
579
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
580
|
+
migrate_up(<<~EOS.strip)
|
581
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
582
|
+
|
583
|
+
add_index :adverts, [:title], unique: true, name: 'my_index'
|
584
|
+
|
585
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
586
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
587
|
+
EOS
|
588
|
+
)
|
589
|
+
|
590
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
591
|
+
|
592
|
+
# You can create an index on more than one field
|
593
|
+
|
594
|
+
class Advert < ActiveRecord::Base
|
595
|
+
index [:title, :category_id]
|
596
|
+
end
|
597
|
+
|
598
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
599
|
+
migrate_up(<<~EOS.strip)
|
600
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
601
|
+
|
602
|
+
add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'
|
603
|
+
|
604
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
605
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
606
|
+
EOS
|
607
|
+
)
|
608
|
+
|
609
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
|
610
|
+
|
611
|
+
# Finally, you can specify that the migration generator should completely ignore an
|
612
|
+
# index by passing its name to ignore_index in the model.
|
613
|
+
# This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
|
614
|
+
|
615
|
+
### Rename a table
|
616
|
+
|
617
|
+
# 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.
|
618
|
+
|
619
|
+
class Advert < ActiveRecord::Base
|
620
|
+
self.table_name = "ads"
|
621
|
+
fields do
|
622
|
+
title :string, limit: 250, null: true
|
623
|
+
body :text, null: true
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
Advert.connection.schema_cache.clear!
|
628
|
+
Advert.reset_column_information
|
629
|
+
|
630
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")).to(
|
631
|
+
migrate_up(<<~EOS.strip)
|
632
|
+
rename_table :adverts, :ads
|
633
|
+
|
634
|
+
add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
|
635
|
+
add_column :ads, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
636
|
+
|
637
|
+
#{if defined?(SQLite3)
|
638
|
+
"add_index :ads, [:id], unique: true, name: 'PRIMARY'\n"
|
639
|
+
elsif defined?(Mysql2)
|
640
|
+
"execute \"ALTER TABLE ads DROP PRIMARY KEY, ADD PRIMARY KEY (id)\"\n\n" +
|
641
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
642
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")"
|
643
|
+
end}
|
644
|
+
EOS
|
645
|
+
.and migrate_down(<<~EOS.strip)
|
646
|
+
remove_column :ads, :title
|
647
|
+
remove_column :ads, :body
|
648
|
+
|
649
|
+
rename_table :ads, :adverts
|
650
|
+
|
651
|
+
#{if defined?(SQLite3)
|
652
|
+
"add_index :adverts, [:id], unique: true, name: 'PRIMARY'\n"
|
653
|
+
elsif defined?(Mysql2)
|
654
|
+
"execute \"ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (id)\"\n\n" +
|
655
|
+
"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" +
|
656
|
+
"remove_foreign_key(\"adverts\", name: \"on_c_id\")"
|
657
|
+
end}
|
658
|
+
EOS
|
659
|
+
)
|
660
|
+
|
661
|
+
# Set the table name back to what it should be and confirm we're in sync:
|
662
|
+
|
663
|
+
nuke_model_class(Advert)
|
664
|
+
|
665
|
+
class Advert < ActiveRecord::Base
|
666
|
+
self.table_name = "adverts"
|
667
|
+
end
|
668
|
+
|
669
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
|
670
|
+
|
671
|
+
### Rename a table
|
672
|
+
|
673
|
+
# 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.
|
674
|
+
|
675
|
+
nuke_model_class(Advert)
|
676
|
+
|
677
|
+
class Advertisement < ActiveRecord::Base
|
678
|
+
fields do
|
679
|
+
title :string, limit: 250, null: true
|
680
|
+
body :text, null: true
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")).to(
|
685
|
+
migrate_up(<<~EOS.strip)
|
686
|
+
rename_table :adverts, :advertisements
|
687
|
+
|
688
|
+
add_column :advertisements, :title, :string, limit: 250, null: true#{charset_and_collation}
|
689
|
+
add_column :advertisements, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
690
|
+
remove_column :advertisements, :name
|
691
|
+
|
692
|
+
#{if defined?(SQLite3)
|
693
|
+
"add_index :advertisements, [:id], unique: true, name: 'PRIMARY'"
|
694
|
+
elsif defined?(Mysql2)
|
695
|
+
"execute \"ALTER TABLE advertisements DROP PRIMARY KEY, ADD PRIMARY KEY (id)\""
|
696
|
+
end}
|
697
|
+
EOS
|
698
|
+
.and migrate_down(<<~EOS.strip)
|
699
|
+
remove_column :advertisements, :title
|
700
|
+
remove_column :advertisements, :body
|
701
|
+
add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
|
702
|
+
|
703
|
+
rename_table :advertisements, :adverts
|
704
|
+
|
705
|
+
#{if defined?(SQLite3)
|
706
|
+
"add_index :adverts, [:id], unique: true, name: 'PRIMARY'"
|
707
|
+
elsif defined?(Mysql2)
|
708
|
+
"execute \"ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (id)\""
|
709
|
+
end}
|
710
|
+
EOS
|
711
|
+
)
|
712
|
+
|
713
|
+
### Drop a table
|
714
|
+
|
715
|
+
nuke_model_class(Advertisement)
|
716
|
+
|
717
|
+
# If you delete a model, the migration generator will create a `drop_table` migration.
|
718
|
+
|
719
|
+
# Dropping tables is where the automatic down-migration really comes in handy:
|
720
|
+
|
721
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
722
|
+
migrate_up(<<~EOS.strip)
|
723
|
+
drop_table :adverts
|
724
|
+
EOS
|
725
|
+
.and migrate_down(<<~EOS.strip)
|
726
|
+
create_table "adverts"#{table_options}, force: :cascade do |t|
|
727
|
+
t.string "name", limit: 250#{charset_and_collation}
|
728
|
+
end
|
729
|
+
EOS
|
730
|
+
)
|
731
|
+
|
732
|
+
## STI
|
733
|
+
|
734
|
+
### Adding an STI subclass
|
735
|
+
|
736
|
+
# Adding a subclass or two should introduce the 'type' column and no other changes
|
737
|
+
|
738
|
+
class Advert < ActiveRecord::Base
|
739
|
+
fields do
|
740
|
+
body :text, null: true
|
741
|
+
title :string, default: "Untitled", limit: 250, null: true
|
742
|
+
end
|
743
|
+
end
|
744
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
745
|
+
ActiveRecord::Migration.class_eval(up)
|
746
|
+
|
747
|
+
class FancyAdvert < Advert
|
748
|
+
end
|
749
|
+
class SuperFancyAdvert < FancyAdvert
|
750
|
+
end
|
751
|
+
|
752
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run do |migrations|
|
753
|
+
expect(migrations).to(
|
754
|
+
migrate_up(<<~EOS.strip)
|
755
|
+
add_column :adverts, :type, :string, limit: 250, null: true#{charset_and_collation}
|
756
|
+
|
757
|
+
add_index :adverts, [:type], name: 'on_type'
|
758
|
+
EOS
|
759
|
+
.and migrate_down(<<~EOS.strip)
|
760
|
+
remove_column :adverts, :type
|
761
|
+
|
762
|
+
remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid
|
763
|
+
EOS
|
764
|
+
)
|
765
|
+
end
|
766
|
+
|
767
|
+
Advert.field_specs.delete(:type)
|
768
|
+
nuke_model_class(SuperFancyAdvert)
|
769
|
+
nuke_model_class(FancyAdvert)
|
770
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["type"] }
|
771
|
+
|
772
|
+
## Coping with multiple changes
|
773
|
+
|
774
|
+
# The migration generator is designed to create complete migrations even if many changes to the models have taken place.
|
775
|
+
|
776
|
+
# First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
|
777
|
+
|
778
|
+
ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
|
779
|
+
Advert.connection.schema_cache.clear!
|
780
|
+
Advert.reset_column_information
|
781
|
+
|
782
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
783
|
+
to eq(["adverts"])
|
784
|
+
expect(Advert.columns.map(&:name).sort).to eq(["body", "id", "title"])
|
785
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
|
786
|
+
|
787
|
+
|
788
|
+
### Rename a column and change the default
|
789
|
+
|
790
|
+
Advert.field_specs.clear
|
791
|
+
|
792
|
+
class Advert < ActiveRecord::Base
|
793
|
+
fields do
|
794
|
+
name :string, default: "No Name", limit: 250, null: true
|
795
|
+
body :text, null: true
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })).to(
|
800
|
+
migrate_up(<<~EOS.strip)
|
801
|
+
rename_column :adverts, :title, :name
|
802
|
+
change_column :adverts, :name, :string, limit: 250, null: true, default: "No Name"#{charset_and_collation}
|
803
|
+
EOS
|
804
|
+
.and migrate_down(<<~EOS.strip)
|
805
|
+
rename_column :adverts, :name, :title
|
806
|
+
change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
|
807
|
+
EOS
|
808
|
+
)
|
809
|
+
|
810
|
+
### Rename a table and add a column
|
811
|
+
|
812
|
+
nuke_model_class(Advert)
|
813
|
+
class Ad < ActiveRecord::Base
|
814
|
+
fields do
|
815
|
+
title :string, default: "Untitled", limit: 250
|
816
|
+
body :text, null: true
|
817
|
+
created_at :datetime
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
821
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)).to(
|
822
|
+
migrate_up(<<~EOS.strip)
|
823
|
+
rename_table :adverts, :ads
|
824
|
+
|
825
|
+
add_column :ads, :created_at, :datetime, null: false
|
826
|
+
change_column :ads, :title, :string, limit: 250, null: false, default: \"Untitled\"#{charset_and_collation}
|
827
|
+
|
828
|
+
#{if defined?(SQLite3)
|
829
|
+
"add_index :ads, [:id], unique: true, name: 'PRIMARY'"
|
830
|
+
elsif defined?(Mysql2)
|
831
|
+
'execute "ALTER TABLE ads DROP PRIMARY KEY, ADD PRIMARY KEY (id)"'
|
832
|
+
end}
|
833
|
+
EOS
|
834
|
+
)
|
835
|
+
|
836
|
+
class Advert < ActiveRecord::Base
|
837
|
+
fields do
|
838
|
+
body :text, null: true
|
839
|
+
title :string, default: "Untitled", limit: 250, null: true
|
840
|
+
end
|
841
|
+
end
|
842
|
+
|
843
|
+
## Legacy Keys
|
844
|
+
|
845
|
+
# DeclareSchema has some support for legacy keys.
|
846
|
+
|
847
|
+
nuke_model_class(Ad)
|
848
|
+
|
849
|
+
class Advert < ActiveRecord::Base
|
850
|
+
fields do
|
851
|
+
body :text, null: true
|
852
|
+
end
|
853
|
+
self.primary_key = "advert_id"
|
854
|
+
end
|
855
|
+
|
856
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })).to(
|
857
|
+
migrate_up(<<~EOS.strip)
|
858
|
+
rename_column :adverts, :id, :advert_id
|
859
|
+
|
860
|
+
#{if defined?(SQLite3)
|
861
|
+
"add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY'"
|
862
|
+
elsif defined?(Mysql2)
|
863
|
+
'execute "ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (advert_id)"'
|
864
|
+
end}
|
865
|
+
EOS
|
866
|
+
)
|
867
|
+
|
868
|
+
nuke_model_class(Advert)
|
869
|
+
ActiveRecord::Base.connection.execute("drop table `adverts`;")
|
870
|
+
|
871
|
+
## DSL
|
872
|
+
|
873
|
+
# The DSL allows lambdas and constants
|
874
|
+
|
875
|
+
class User < ActiveRecord::Base
|
876
|
+
fields do
|
877
|
+
company :string, limit: 250, ruby_default: -> { "BigCorp" }
|
878
|
+
end
|
879
|
+
end
|
880
|
+
expect(User.field_specs.keys).to eq(['company'])
|
881
|
+
expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
|
882
|
+
|
883
|
+
## validates
|
884
|
+
|
885
|
+
# DeclareSchema can accept a validates hash in the field options.
|
886
|
+
|
887
|
+
class Ad < ActiveRecord::Base
|
888
|
+
class << self
|
889
|
+
def validates(field_name, options)
|
890
|
+
end
|
891
|
+
end
|
892
|
+
end
|
893
|
+
expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
|
894
|
+
class Ad < ActiveRecord::Base
|
895
|
+
fields do
|
896
|
+
company :string, limit: 250, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
|
897
|
+
end
|
898
|
+
self.primary_key = "advert_id"
|
899
|
+
end
|
900
|
+
up, _down = Generators::DeclareSchema::Migration::Migrator.run
|
901
|
+
ActiveRecord::Migration.class_eval(up)
|
902
|
+
expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
|
903
|
+
end
|
904
|
+
|
905
|
+
describe 'serialize' do
|
906
|
+
before do
|
907
|
+
class Ad < ActiveRecord::Base
|
908
|
+
@serialize_args = []
|
909
|
+
|
910
|
+
class << self
|
911
|
+
attr_reader :serialize_args
|
912
|
+
|
913
|
+
def serialize(*args)
|
914
|
+
@serialize_args << args
|
915
|
+
end
|
916
|
+
end
|
917
|
+
end
|
918
|
+
end
|
919
|
+
|
920
|
+
describe 'untyped' do
|
921
|
+
it 'allows serialize: true' do
|
922
|
+
class Ad < ActiveRecord::Base
|
923
|
+
fields do
|
924
|
+
allow_list :text, limit: 0xFFFF, serialize: true
|
925
|
+
end
|
926
|
+
end
|
927
|
+
|
928
|
+
expect(Ad.serialize_args).to eq([[:allow_list]])
|
929
|
+
end
|
930
|
+
|
931
|
+
it 'converts defaults with .to_yaml' do
|
932
|
+
class Ad < ActiveRecord::Base
|
933
|
+
fields do
|
934
|
+
allow_list :string, limit: 250, serialize: true, null: true, default: []
|
935
|
+
allow_hash :string, limit: 250, serialize: true, null: true, default: {}
|
936
|
+
allow_string :string, limit: 250, serialize: true, null: true, default: ['abc']
|
937
|
+
allow_null :string, limit: 250, serialize: true, null: true, default: nil
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
expect(Ad.field_specs['allow_list'].default).to eq("--- []\n")
|
942
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("--- {}\n")
|
943
|
+
expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
|
944
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
945
|
+
end
|
946
|
+
end
|
947
|
+
|
948
|
+
describe 'Array' do
|
949
|
+
it 'allows serialize: Array' do
|
950
|
+
class Ad < ActiveRecord::Base
|
951
|
+
fields do
|
952
|
+
allow_list :string, limit: 250, serialize: Array, null: true
|
953
|
+
end
|
954
|
+
end
|
955
|
+
|
956
|
+
expect(Ad.serialize_args).to eq([[:allow_list, Array]])
|
957
|
+
end
|
958
|
+
|
959
|
+
it 'allows Array defaults' do
|
960
|
+
class Ad < ActiveRecord::Base
|
961
|
+
fields do
|
962
|
+
allow_list :string, limit: 250, serialize: Array, null: true, default: [2]
|
963
|
+
allow_string :string, limit: 250, serialize: Array, null: true, default: ['abc']
|
964
|
+
allow_empty :string, limit: 250, serialize: Array, null: true, default: []
|
965
|
+
allow_null :string, limit: 250, serialize: Array, null: true, default: nil
|
966
|
+
end
|
967
|
+
end
|
968
|
+
|
969
|
+
expect(Ad.field_specs['allow_list'].default).to eq("---\n- 2\n")
|
970
|
+
expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
|
971
|
+
expect(Ad.field_specs['allow_empty'].default).to eq(nil)
|
972
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
973
|
+
end
|
974
|
+
end
|
975
|
+
|
976
|
+
describe 'Hash' do
|
977
|
+
it 'allows serialize: Hash' do
|
978
|
+
class Ad < ActiveRecord::Base
|
979
|
+
fields do
|
980
|
+
allow_list :string, limit: 250, serialize: Hash, null: true
|
981
|
+
end
|
982
|
+
end
|
983
|
+
|
984
|
+
expect(Ad.serialize_args).to eq([[:allow_list, Hash]])
|
985
|
+
end
|
986
|
+
|
987
|
+
it 'allows Hash defaults' do
|
988
|
+
class Ad < ActiveRecord::Base
|
989
|
+
fields do
|
990
|
+
allow_loc :string, limit: 250, serialize: Hash, null: true, default: { 'state' => 'CA' }
|
991
|
+
allow_hash :string, limit: 250, serialize: Hash, null: true, default: {}
|
992
|
+
allow_null :string, limit: 250, serialize: Hash, null: true, default: nil
|
993
|
+
end
|
994
|
+
end
|
995
|
+
|
996
|
+
expect(Ad.field_specs['allow_loc'].default).to eq("---\nstate: CA\n")
|
997
|
+
expect(Ad.field_specs['allow_hash'].default).to eq(nil)
|
998
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
999
|
+
end
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
describe 'JSON' do
|
1003
|
+
it 'allows serialize: JSON' do
|
1004
|
+
class Ad < ActiveRecord::Base
|
1005
|
+
fields do
|
1006
|
+
allow_list :string, limit: 250, serialize: JSON
|
1007
|
+
end
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
expect(Ad.serialize_args).to eq([[:allow_list, JSON]])
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
it 'allows JSON defaults' do
|
1014
|
+
class Ad < ActiveRecord::Base
|
1015
|
+
fields do
|
1016
|
+
allow_hash :string, limit: 250, serialize: JSON, null: true, default: { 'state' => 'CA' }
|
1017
|
+
allow_empty_array :string, limit: 250, serialize: JSON, null: true, default: []
|
1018
|
+
allow_empty_hash :string, limit: 250, serialize: JSON, null: true, default: {}
|
1019
|
+
allow_null :string, limit: 250, serialize: JSON, null: true, default: nil
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("{\"state\":\"CA\"}")
|
1024
|
+
expect(Ad.field_specs['allow_empty_array'].default).to eq("[]")
|
1025
|
+
expect(Ad.field_specs['allow_empty_hash'].default).to eq("{}")
|
1026
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
class ValueClass
|
1031
|
+
delegate :present?, :inspect, to: :@value
|
1032
|
+
|
1033
|
+
def initialize(value)
|
1034
|
+
@value = value
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
class << self
|
1038
|
+
def dump(object)
|
1039
|
+
if object&.present?
|
1040
|
+
object.inspect
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
def load(serialized)
|
1045
|
+
if serialized
|
1046
|
+
raise 'not used ???'
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
describe 'custom coder' do
|
1053
|
+
it 'allows serialize: ValueClass' do
|
1054
|
+
class Ad < ActiveRecord::Base
|
1055
|
+
fields do
|
1056
|
+
allow_list :string, limit: 250, serialize: ValueClass
|
1057
|
+
end
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
expect(Ad.serialize_args).to eq([[:allow_list, ValueClass]])
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
it 'allows ValueClass defaults' do
|
1064
|
+
class Ad < ActiveRecord::Base
|
1065
|
+
fields do
|
1066
|
+
allow_hash :string, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([2])
|
1067
|
+
allow_empty_array :string, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([])
|
1068
|
+
allow_null :string, limit: 250, serialize: ValueClass, null: true, default: nil
|
1069
|
+
end
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("[2]")
|
1073
|
+
expect(Ad.field_specs['allow_empty_array'].default).to eq(nil)
|
1074
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
it 'disallows serialize: with a non-string column type' do
|
1079
|
+
expect do
|
1080
|
+
class Ad < ActiveRecord::Base
|
1081
|
+
fields do
|
1082
|
+
allow_list :integer, limit: 8, serialize: true
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
end.to raise_exception(ArgumentError, /must be :string or :text/)
|
1086
|
+
end
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
context "for Rails #{Rails::VERSION::MAJOR}" do
|
1090
|
+
if Rails::VERSION::MAJOR >= 5
|
1091
|
+
let(:optional_true) { { optional: true } }
|
1092
|
+
let(:optional_false) { { optional: false } }
|
1093
|
+
else
|
1094
|
+
let(:optional_true) { {} }
|
1095
|
+
let(:optional_false) { {} }
|
1096
|
+
end
|
1097
|
+
let(:optional_flag) { { false => optional_false, true => optional_true } }
|
1098
|
+
|
1099
|
+
describe 'belongs_to' do
|
1100
|
+
before do
|
1101
|
+
unless defined?(AdCategory)
|
1102
|
+
class AdCategory < ActiveRecord::Base
|
1103
|
+
fields { }
|
1104
|
+
end
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
class Advert < ActiveRecord::Base
|
1108
|
+
fields do
|
1109
|
+
name :string, limit: 250, null: true
|
1110
|
+
category_id :integer, limit: 8
|
1111
|
+
nullable_category_id :integer, limit: 8, null: true
|
1112
|
+
end
|
1113
|
+
end
|
1114
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
1115
|
+
ActiveRecord::Migration.class_eval(up)
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
it 'passes through optional: when given' do
|
1119
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
1120
|
+
self.table_name = 'adverts'
|
1121
|
+
fields { }
|
1122
|
+
reset_column_information
|
1123
|
+
belongs_to :ad_category, optional: true
|
1124
|
+
end
|
1125
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
describe 'contradictory settings' do # contradictory settings are ok--for example, during migration
|
1129
|
+
it 'passes through optional: true, null: false' do
|
1130
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
1131
|
+
self.table_name = 'adverts'
|
1132
|
+
fields { }
|
1133
|
+
reset_column_information
|
1134
|
+
belongs_to :ad_category, optional: true, null: false
|
1135
|
+
end
|
1136
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
|
1137
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(false)
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
it 'passes through optional: false, null: true' do
|
1141
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
1142
|
+
self.table_name = 'adverts'
|
1143
|
+
fields { }
|
1144
|
+
reset_column_information
|
1145
|
+
belongs_to :ad_category, optional: false, null: true
|
1146
|
+
end
|
1147
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_false)
|
1148
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(true)
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
[false, true].each do |nullable|
|
1153
|
+
context "nullable=#{nullable}" do
|
1154
|
+
it 'infers optional: from null:' do
|
1155
|
+
eval <<~EOS
|
1156
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
1157
|
+
fields { }
|
1158
|
+
belongs_to :ad_category, null: #{nullable}
|
1159
|
+
end
|
1160
|
+
EOS
|
1161
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
|
1162
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
it 'infers null: from optional:' do
|
1166
|
+
eval <<~EOS
|
1167
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
1168
|
+
fields { }
|
1169
|
+
belongs_to :ad_category, optional: #{nullable}
|
1170
|
+
end
|
1171
|
+
EOS
|
1172
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
|
1173
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
1174
|
+
end
|
1175
|
+
end
|
1176
|
+
end
|
1177
|
+
end
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
describe 'migration base class' do
|
1181
|
+
it 'adapts to Rails 4' do
|
1182
|
+
class Advert < active_record_base_class.constantize
|
1183
|
+
fields do
|
1184
|
+
title :string, limit: 100
|
1185
|
+
end
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
generate_migrations '-n', '-m'
|
1189
|
+
|
1190
|
+
migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
|
1191
|
+
expect(migrations.size).to eq(1), migrations.inspect
|
1192
|
+
|
1193
|
+
migration_content = File.read(migrations.first)
|
1194
|
+
first_line = migration_content.split("\n").first
|
1195
|
+
base_class = first_line.split(' < ').last
|
1196
|
+
expect(base_class).to eq("(Rails::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration)")
|
1197
|
+
end
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
context 'Does not generate migrations' do
|
1201
|
+
it 'for aliased fields bigint -> integer limit 8' do
|
1202
|
+
if Rails::VERSION::MAJOR >= 5 || !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
1203
|
+
class Advert < active_record_base_class.constantize
|
1204
|
+
fields do
|
1205
|
+
price :bigint
|
1206
|
+
end
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
generate_migrations '-n', '-m'
|
1210
|
+
|
1211
|
+
migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
|
1212
|
+
expect(migrations.size).to eq(1), migrations.inspect
|
1213
|
+
|
1214
|
+
if defined?(Mysql2) && Rails::VERSION::MAJOR < 5
|
1215
|
+
ActiveRecord::Base.connection.execute("ALTER TABLE adverts ADD PRIMARY KEY (id)")
|
1216
|
+
end
|
1217
|
+
|
1218
|
+
class Advert < active_record_base_class.constantize
|
1219
|
+
fields do
|
1220
|
+
price :integer, limit: 8
|
1221
|
+
end
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
expect { generate_migrations '-n', '-g' }.to output("Database and models match -- nothing to change\n").to_stdout
|
1225
|
+
end
|
1226
|
+
end
|
1227
|
+
end
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
context 'Using declare_schema' do
|
1231
|
+
# DeclareSchema - Migration Generator
|
1232
|
+
it 'generates migrations' do
|
1233
|
+
## The migration generator -- introduction
|
1234
|
+
|
1235
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
|
1236
|
+
|
1237
|
+
class Advert < ActiveRecord::Base
|
1238
|
+
end
|
1239
|
+
|
1240
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
|
1241
|
+
|
1242
|
+
Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
|
1243
|
+
|
1244
|
+
Advert.connection.schema_cache.clear!
|
1245
|
+
Advert.reset_column_information
|
1246
|
+
|
1247
|
+
class Advert < ActiveRecord::Base
|
1248
|
+
declare_schema do
|
1249
|
+
string :name, limit: 250, null: true
|
1250
|
+
end
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
|
1254
|
+
expect(migrations).to(
|
1255
|
+
migrate_up(<<~EOS.strip)
|
1256
|
+
create_table :adverts, id: :bigint do |t|
|
1257
|
+
t.string :name, limit: 250, null: true#{charset_and_collation}
|
1258
|
+
end#{charset_alter_table}
|
1259
|
+
EOS
|
1260
|
+
.and migrate_down("drop_table :adverts")
|
1261
|
+
)
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
ActiveRecord::Migration.class_eval(up)
|
1265
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "name"])
|
1266
|
+
|
1267
|
+
if Rails::VERSION::MAJOR < 5
|
1268
|
+
# Rails 4 drivers don't always create PK properly. Fix that by dropping and recreating.
|
1269
|
+
ActiveRecord::Base.connection.execute("drop table adverts")
|
1270
|
+
if defined?(Mysql2)
|
1271
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE adverts (id integer PRIMARY KEY AUTO_INCREMENT NOT NULL, name varchar(250)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin")
|
1272
|
+
else
|
1273
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE adverts (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, name varchar(250))")
|
1274
|
+
end
|
1275
|
+
end
|
1276
|
+
|
1277
|
+
class Advert < ActiveRecord::Base
|
1278
|
+
declare_schema do
|
1279
|
+
string :name, limit: 250, null: true
|
1280
|
+
text :body, null: true
|
1281
|
+
datetime :published_at, null: true
|
1282
|
+
end
|
1283
|
+
end
|
1284
|
+
|
1285
|
+
Advert.connection.schema_cache.clear!
|
1286
|
+
Advert.reset_column_information
|
1287
|
+
|
1288
|
+
expect(migrate).to(
|
1289
|
+
migrate_up(<<~EOS.strip)
|
1290
|
+
add_column :adverts, :body, :text#{text_limit}, null: true#{charset_and_collation}
|
1291
|
+
add_column :adverts, :published_at, :datetime, null: true
|
1292
|
+
EOS
|
1293
|
+
.and migrate_down(<<~EOS.strip)
|
1294
|
+
remove_column :adverts, :body
|
1295
|
+
remove_column :adverts, :published_at
|
1296
|
+
EOS
|
1297
|
+
)
|
1298
|
+
|
1299
|
+
Advert.field_specs.clear # not normally needed
|
1300
|
+
class Advert < ActiveRecord::Base
|
1301
|
+
declare_schema do
|
1302
|
+
string :name, limit: 250, null: true
|
1303
|
+
text :body, null: true
|
1304
|
+
end
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
expect(migrate).to(
|
1308
|
+
migrate_up("remove_column :adverts, :published_at").and(
|
1309
|
+
migrate_down("add_column :adverts, :published_at, :datetime#{datetime_precision}, null: true")
|
1310
|
+
)
|
1311
|
+
)
|
1312
|
+
|
1313
|
+
nuke_model_class(Advert)
|
1314
|
+
class Advert < ActiveRecord::Base
|
1315
|
+
declare_schema do
|
1316
|
+
string :title, limit: 250, null: true
|
1317
|
+
text :body, null: true
|
1318
|
+
end
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1322
|
+
migrate_up(<<~EOS.strip)
|
1323
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
1324
|
+
remove_column :adverts, :name
|
1325
|
+
EOS
|
1326
|
+
.and migrate_down(<<~EOS.strip)
|
1327
|
+
remove_column :adverts, :title
|
1328
|
+
add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
|
1329
|
+
EOS
|
1330
|
+
)
|
1331
|
+
|
1332
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })).to(
|
1333
|
+
migrate_up("rename_column :adverts, :name, :title").and(
|
1334
|
+
migrate_down("rename_column :adverts, :title, :name")
|
1335
|
+
)
|
1336
|
+
)
|
1337
|
+
|
1338
|
+
migrate
|
1339
|
+
|
1340
|
+
class Advert < ActiveRecord::Base
|
1341
|
+
declare_schema do
|
1342
|
+
text :title, null: true
|
1343
|
+
text :body, null: true
|
1344
|
+
end
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1348
|
+
migrate_up("change_column :adverts, :title, :text#{text_limit}, null: true#{charset_and_collation}").and(
|
1349
|
+
migrate_down("change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}")
|
1350
|
+
)
|
1351
|
+
)
|
1352
|
+
|
1353
|
+
class Advert < ActiveRecord::Base
|
1354
|
+
declare_schema do
|
1355
|
+
string :title, default: "Untitled", limit: 250, null: true
|
1356
|
+
text :body, null: true
|
1357
|
+
end
|
1358
|
+
end
|
1359
|
+
|
1360
|
+
expect(migrate).to(
|
1361
|
+
migrate_up(<<~EOS.strip)
|
1362
|
+
change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
|
1363
|
+
EOS
|
1364
|
+
.and migrate_down(<<~EOS.strip)
|
1365
|
+
change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
1366
|
+
EOS
|
1367
|
+
)
|
1368
|
+
|
1369
|
+
### Limits
|
1370
|
+
|
1371
|
+
class Advert < ActiveRecord::Base
|
1372
|
+
declare_schema do
|
1373
|
+
integer :price, null: true, limit: 2
|
1374
|
+
end
|
1375
|
+
end
|
1376
|
+
|
1377
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
|
1378
|
+
expect(migrations).to migrate_up("add_column :adverts, :price, :integer, limit: 2, null: true")
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
# Now run the migration, then change the limit:
|
1382
|
+
|
1383
|
+
ActiveRecord::Migration.class_eval(up)
|
1384
|
+
class Advert < ActiveRecord::Base
|
1385
|
+
declare_schema do
|
1386
|
+
integer :price, null: true, limit: 3
|
1387
|
+
end
|
1388
|
+
end
|
1389
|
+
|
1390
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1391
|
+
migrate_up(<<~EOS.strip)
|
1392
|
+
change_column :adverts, :price, :integer, limit: 3, null: true
|
1393
|
+
EOS
|
1394
|
+
.and migrate_down(<<~EOS.strip)
|
1395
|
+
change_column :adverts, :price, :integer, limit: 2, null: true
|
1396
|
+
EOS
|
1397
|
+
)
|
1398
|
+
|
1399
|
+
ActiveRecord::Migration.class_eval("remove_column :adverts, :price")
|
1400
|
+
class Advert < ActiveRecord::Base
|
1401
|
+
declare_schema do
|
1402
|
+
decimal :price, precision: 4, scale: 1, null: true
|
1403
|
+
end
|
1404
|
+
end
|
1405
|
+
|
1406
|
+
# Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
|
1407
|
+
# allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
|
1408
|
+
# If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
|
1409
|
+
# that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
|
1410
|
+
|
1411
|
+
if defined?(SQLite3)
|
1412
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_falsey
|
1413
|
+
end
|
1414
|
+
|
1415
|
+
class Advert < ActiveRecord::Base
|
1416
|
+
declare_schema do
|
1417
|
+
text :notes
|
1418
|
+
text :description, limit: 30000
|
1419
|
+
end
|
1420
|
+
end
|
1421
|
+
|
1422
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1423
|
+
migrate_up(<<~EOS.strip)
|
1424
|
+
add_column :adverts, :price, :decimal, precision: 4, scale: 1, null: true
|
1425
|
+
add_column :adverts, :notes, :text#{text_limit}, null: false#{charset_and_collation}
|
1426
|
+
add_column :adverts, :description, :text#{', limit: 65535' if defined?(Mysql2)}, null: false#{charset_and_collation}
|
1427
|
+
EOS
|
1428
|
+
)
|
1429
|
+
|
1430
|
+
Advert.field_specs.delete :price
|
1431
|
+
Advert.field_specs.delete :notes
|
1432
|
+
Advert.field_specs.delete :description
|
1433
|
+
|
1434
|
+
# In MySQL, limits are applied, rounded up:
|
1435
|
+
|
1436
|
+
if defined?(Mysql2)
|
1437
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
1438
|
+
|
1439
|
+
class Advert < ActiveRecord::Base
|
1440
|
+
declare_schema do
|
1441
|
+
text :notes
|
1442
|
+
text :description, limit: 250
|
1443
|
+
end
|
1444
|
+
end
|
1445
|
+
|
1446
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1447
|
+
migrate_up(<<~EOS.strip)
|
1448
|
+
add_column :adverts, :notes, :text, limit: 4294967295, null: false#{charset_and_collation}
|
1449
|
+
add_column :adverts, :description, :text, limit: 255, null: false#{charset_and_collation}
|
1450
|
+
EOS
|
1451
|
+
)
|
1452
|
+
|
1453
|
+
Advert.field_specs.delete :notes
|
1454
|
+
|
1455
|
+
# Limits that are too high for MySQL will raise an exception.
|
1456
|
+
|
1457
|
+
expect do
|
1458
|
+
class Advert < ActiveRecord::Base
|
1459
|
+
declare_schema do
|
1460
|
+
text :notes
|
1461
|
+
text :description, limit: 0x1_0000_0000
|
1462
|
+
end
|
1463
|
+
end
|
1464
|
+
end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
|
1465
|
+
|
1466
|
+
Advert.field_specs.delete :notes
|
1467
|
+
|
1468
|
+
# And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
|
1469
|
+
|
1470
|
+
# To start, we'll set the database schema for `description` to match the above limit of 250.
|
1471
|
+
|
1472
|
+
Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
|
1473
|
+
Advert.connection.schema_cache.clear!
|
1474
|
+
Advert.reset_column_information
|
1475
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
1476
|
+
to eq(["adverts"])
|
1477
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "body", "title", "description"])
|
1478
|
+
|
1479
|
+
# Now migrate to an unstated text limit:
|
1480
|
+
|
1481
|
+
class Advert < ActiveRecord::Base
|
1482
|
+
declare_schema do
|
1483
|
+
text :description
|
1484
|
+
end
|
1485
|
+
end
|
1486
|
+
|
1487
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1488
|
+
migrate_up(<<~EOS.strip)
|
1489
|
+
change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
|
1490
|
+
EOS
|
1491
|
+
.and migrate_down(<<~EOS.strip)
|
1492
|
+
change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
1493
|
+
EOS
|
1494
|
+
)
|
1495
|
+
|
1496
|
+
# And migrate to a stated text limit that is the same as the unstated one:
|
324
1497
|
|
325
|
-
|
326
|
-
|
327
|
-
|
1498
|
+
class Advert < ActiveRecord::Base
|
1499
|
+
declare_schema do
|
1500
|
+
text :description, limit: 0xffffffff
|
1501
|
+
end
|
328
1502
|
end
|
329
|
-
end
|
330
1503
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
1504
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1505
|
+
migrate_up(<<~EOS.strip)
|
1506
|
+
change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
|
1507
|
+
EOS
|
1508
|
+
.and migrate_down(<<~EOS.strip)
|
1509
|
+
change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
337
1510
|
EOS
|
338
|
-
|
339
|
-
|
1511
|
+
)
|
1512
|
+
end
|
340
1513
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
1514
|
+
Advert.field_specs.clear
|
1515
|
+
Advert.connection.schema_cache.clear!
|
1516
|
+
Advert.reset_column_information
|
1517
|
+
class Advert < ActiveRecord::Base
|
1518
|
+
declare_schema do
|
1519
|
+
string :name, limit: 250, null: true
|
1520
|
+
end
|
347
1521
|
end
|
348
|
-
end
|
349
1522
|
|
350
|
-
|
351
|
-
|
1523
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
1524
|
+
ActiveRecord::Migration.class_eval up
|
352
1525
|
|
353
|
-
|
354
|
-
|
1526
|
+
Advert.connection.schema_cache.clear!
|
1527
|
+
Advert.reset_column_information
|
355
1528
|
|
356
|
-
|
1529
|
+
### Foreign Keys
|
357
1530
|
|
358
|
-
|
359
|
-
|
1531
|
+
# DeclareSchema extends the `belongs_to` macro so that it also declares the
|
1532
|
+
# foreign-key field. It also generates an index on the field.
|
360
1533
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
1534
|
+
class Category < ActiveRecord::Base; end
|
1535
|
+
class Advert < ActiveRecord::Base
|
1536
|
+
declare_schema do
|
1537
|
+
string :name, limit: 250, null: true
|
1538
|
+
end
|
1539
|
+
belongs_to :category
|
365
1540
|
end
|
366
|
-
belongs_to :category
|
367
|
-
end
|
368
1541
|
|
369
|
-
|
370
|
-
|
371
|
-
|
1542
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1543
|
+
migrate_up(<<~EOS.strip)
|
1544
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
372
1545
|
|
373
|
-
|
1546
|
+
add_index :adverts, [:category_id], name: 'on_category_id'
|
374
1547
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
1548
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" if defined?(Mysql2)}
|
1549
|
+
EOS
|
1550
|
+
.and migrate_down(<<~EOS.strip)
|
1551
|
+
remove_column :adverts, :category_id
|
379
1552
|
|
380
|
-
|
1553
|
+
remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid
|
381
1554
|
|
382
|
-
|
1555
|
+
#{"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" if defined?(Mysql2)}
|
383
1556
|
EOS
|
384
|
-
|
1557
|
+
)
|
385
1558
|
|
386
|
-
|
387
|
-
|
1559
|
+
Advert.field_specs.delete(:category_id)
|
1560
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
|
388
1561
|
|
389
|
-
|
1562
|
+
# If you specify a custom foreign key, the migration generator observes that:
|
390
1563
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
1564
|
+
class Category < ActiveRecord::Base; end
|
1565
|
+
class Advert < ActiveRecord::Base
|
1566
|
+
declare_schema { }
|
1567
|
+
belongs_to :category, foreign_key: "c_id", class_name: 'Category'
|
1568
|
+
end
|
396
1569
|
|
397
|
-
|
398
|
-
|
399
|
-
|
1570
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1571
|
+
migrate_up(<<~EOS.strip)
|
1572
|
+
add_column :adverts, :c_id, :integer, limit: 8, null: false
|
400
1573
|
|
401
|
-
|
1574
|
+
add_index :adverts, [:c_id], name: 'on_c_id'
|
402
1575
|
|
403
|
-
|
1576
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
404
1577
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
405
1578
|
EOS
|
406
|
-
|
1579
|
+
)
|
407
1580
|
|
408
|
-
|
409
|
-
|
1581
|
+
Advert.field_specs.delete(:c_id)
|
1582
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
|
410
1583
|
|
411
|
-
|
1584
|
+
# You can avoid generating the index by specifying `index: false`
|
412
1585
|
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
1586
|
+
class Category < ActiveRecord::Base; end
|
1587
|
+
class Advert < ActiveRecord::Base
|
1588
|
+
declare_schema { }
|
1589
|
+
belongs_to :category, index: false
|
1590
|
+
end
|
418
1591
|
|
419
|
-
|
420
|
-
|
421
|
-
|
1592
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1593
|
+
migrate_up(<<~EOS.strip)
|
1594
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
422
1595
|
|
423
|
-
|
1596
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
424
1597
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
425
1598
|
EOS
|
426
|
-
|
1599
|
+
)
|
427
1600
|
|
428
|
-
|
429
|
-
|
1601
|
+
Advert.field_specs.delete(:category_id)
|
1602
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
430
1603
|
|
431
|
-
|
1604
|
+
# You can specify the index name with :index
|
432
1605
|
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
1606
|
+
class Category < ActiveRecord::Base; end
|
1607
|
+
class Advert < ActiveRecord::Base
|
1608
|
+
declare_schema { }
|
1609
|
+
belongs_to :category, index: 'my_index'
|
1610
|
+
end
|
438
1611
|
|
439
|
-
|
440
|
-
|
441
|
-
|
1612
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1613
|
+
migrate_up(<<~EOS.strip)
|
1614
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
442
1615
|
|
443
|
-
|
1616
|
+
add_index :adverts, [:category_id], name: 'my_index'
|
444
1617
|
|
445
|
-
|
1618
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
446
1619
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
447
1620
|
EOS
|
448
|
-
|
1621
|
+
)
|
449
1622
|
|
450
|
-
|
451
|
-
|
1623
|
+
Advert.field_specs.delete(:category_id)
|
1624
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
452
1625
|
|
453
|
-
|
1626
|
+
### Timestamps and Optimimistic Locking
|
454
1627
|
|
455
|
-
|
456
|
-
|
1628
|
+
# `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
|
1629
|
+
# Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
|
457
1630
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
1631
|
+
class Advert < ActiveRecord::Base
|
1632
|
+
declare_schema do
|
1633
|
+
timestamps
|
1634
|
+
optimistic_lock
|
1635
|
+
end
|
462
1636
|
end
|
463
|
-
end
|
464
|
-
|
465
|
-
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
466
|
-
migrate_up(<<~EOS.strip)
|
467
|
-
add_column :adverts, :created_at, :datetime, null: true
|
468
|
-
add_column :adverts, :updated_at, :datetime, null: true
|
469
|
-
add_column :adverts, :lock_version, :integer#{lock_version_limit}, null: false, default: 1
|
470
|
-
|
471
|
-
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
472
|
-
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
473
|
-
EOS
|
474
|
-
.and migrate_down(<<~EOS.strip)
|
475
|
-
remove_column :adverts, :created_at
|
476
|
-
remove_column :adverts, :updated_at
|
477
|
-
remove_column :adverts, :lock_version
|
478
1637
|
|
479
|
-
|
1638
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1639
|
+
migrate_up(<<~EOS.strip)
|
1640
|
+
add_column :adverts, :created_at, :datetime, null: true
|
1641
|
+
add_column :adverts, :updated_at, :datetime, null: true
|
1642
|
+
add_column :adverts, :lock_version, :integer#{lock_version_limit}, null: false, default: 1
|
1643
|
+
|
1644
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
1645
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
1646
|
+
EOS
|
1647
|
+
.and migrate_down(<<~EOS.strip)
|
1648
|
+
remove_column :adverts, :created_at
|
1649
|
+
remove_column :adverts, :updated_at
|
1650
|
+
remove_column :adverts, :lock_version
|
1651
|
+
|
1652
|
+
#{"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" +
|
480
1653
|
"remove_foreign_key(\"adverts\", name: \"on_c_id\")" if defined?(Mysql2)}
|
481
1654
|
EOS
|
482
|
-
|
1655
|
+
)
|
483
1656
|
|
484
|
-
|
485
|
-
|
486
|
-
|
1657
|
+
Advert.field_specs.delete(:updated_at)
|
1658
|
+
Advert.field_specs.delete(:created_at)
|
1659
|
+
Advert.field_specs.delete(:lock_version)
|
487
1660
|
|
488
|
-
|
1661
|
+
### Indices
|
489
1662
|
|
490
|
-
|
1663
|
+
# You can add an index to a field definition
|
491
1664
|
|
492
|
-
|
493
|
-
|
494
|
-
|
1665
|
+
class Advert < ActiveRecord::Base
|
1666
|
+
declare_schema do
|
1667
|
+
string :title, index: true, limit: 250, null: true
|
1668
|
+
end
|
495
1669
|
end
|
496
|
-
end
|
497
1670
|
|
498
|
-
|
499
|
-
|
500
|
-
|
1671
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1672
|
+
migrate_up(<<~EOS.strip)
|
1673
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
501
1674
|
|
502
|
-
|
1675
|
+
add_index :adverts, [:title], name: 'on_title'
|
503
1676
|
|
504
|
-
|
1677
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
505
1678
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
506
1679
|
EOS
|
507
|
-
|
1680
|
+
)
|
508
1681
|
|
509
|
-
|
1682
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
510
1683
|
|
511
|
-
|
1684
|
+
# You can ask for a unique index
|
512
1685
|
|
513
|
-
|
514
|
-
|
515
|
-
|
1686
|
+
class Advert < ActiveRecord::Base
|
1687
|
+
declare_schema do
|
1688
|
+
string :title, index: true, unique: true, null: true, limit: 250
|
1689
|
+
end
|
516
1690
|
end
|
517
|
-
end
|
518
1691
|
|
519
|
-
|
520
|
-
|
521
|
-
|
1692
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1693
|
+
migrate_up(<<~EOS.strip)
|
1694
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
522
1695
|
|
523
|
-
|
1696
|
+
add_index :adverts, [:title], unique: true, name: 'on_title'
|
524
1697
|
|
525
|
-
|
1698
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
526
1699
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
527
1700
|
EOS
|
528
|
-
|
1701
|
+
)
|
529
1702
|
|
530
|
-
|
1703
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
531
1704
|
|
532
|
-
|
1705
|
+
# You can specify the name for the index
|
533
1706
|
|
534
|
-
|
535
|
-
|
536
|
-
|
1707
|
+
class Advert < ActiveRecord::Base
|
1708
|
+
declare_schema do
|
1709
|
+
string :title, index: 'my_index', limit: 250, null: true
|
1710
|
+
end
|
537
1711
|
end
|
538
|
-
end
|
539
1712
|
|
540
|
-
|
541
|
-
|
542
|
-
|
1713
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1714
|
+
migrate_up(<<~EOS.strip)
|
1715
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
543
1716
|
|
544
|
-
|
1717
|
+
add_index :adverts, [:title], name: 'my_index'
|
545
1718
|
|
546
|
-
|
1719
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
547
1720
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
548
1721
|
EOS
|
549
|
-
|
1722
|
+
)
|
550
1723
|
|
551
|
-
|
1724
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
552
1725
|
|
553
|
-
|
1726
|
+
# You can ask for an index outside of the fields block
|
554
1727
|
|
555
|
-
|
556
|
-
|
557
|
-
|
1728
|
+
class Advert < ActiveRecord::Base
|
1729
|
+
index :title
|
1730
|
+
end
|
558
1731
|
|
559
|
-
|
560
|
-
|
561
|
-
|
1732
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1733
|
+
migrate_up(<<~EOS.strip)
|
1734
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
562
1735
|
|
563
|
-
|
1736
|
+
add_index :adverts, [:title], name: 'on_title'
|
564
1737
|
|
565
|
-
|
1738
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
566
1739
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
567
1740
|
EOS
|
568
|
-
|
1741
|
+
)
|
569
1742
|
|
570
|
-
|
1743
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
571
1744
|
|
572
|
-
|
1745
|
+
# The available options for the index function are `:unique` and `:name`
|
573
1746
|
|
574
|
-
|
575
|
-
|
576
|
-
|
1747
|
+
class Advert < ActiveRecord::Base
|
1748
|
+
index :title, unique: true, name: 'my_index'
|
1749
|
+
end
|
577
1750
|
|
578
|
-
|
579
|
-
|
580
|
-
|
1751
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1752
|
+
migrate_up(<<~EOS.strip)
|
1753
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
581
1754
|
|
582
|
-
|
1755
|
+
add_index :adverts, [:title], unique: true, name: 'my_index'
|
583
1756
|
|
584
|
-
|
1757
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
585
1758
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
586
1759
|
EOS
|
587
|
-
|
1760
|
+
)
|
588
1761
|
|
589
|
-
|
1762
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
590
1763
|
|
591
|
-
|
1764
|
+
# You can create an index on more than one field
|
592
1765
|
|
593
|
-
|
594
|
-
|
595
|
-
|
1766
|
+
class Advert < ActiveRecord::Base
|
1767
|
+
index [:title, :category_id]
|
1768
|
+
end
|
596
1769
|
|
597
|
-
|
598
|
-
|
599
|
-
|
1770
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1771
|
+
migrate_up(<<~EOS.strip)
|
1772
|
+
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
600
1773
|
|
601
|
-
|
1774
|
+
add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'
|
602
1775
|
|
603
|
-
|
1776
|
+
#{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
604
1777
|
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
|
605
1778
|
EOS
|
606
|
-
|
1779
|
+
)
|
607
1780
|
|
608
|
-
|
1781
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
|
609
1782
|
|
610
|
-
|
611
|
-
|
612
|
-
|
1783
|
+
# Finally, you can specify that the migration generator should completely ignore an
|
1784
|
+
# index by passing its name to ignore_index in the model.
|
1785
|
+
# This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
|
613
1786
|
|
614
|
-
|
1787
|
+
### Rename a table
|
615
1788
|
|
616
|
-
|
1789
|
+
# 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.
|
617
1790
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
1791
|
+
class Advert < ActiveRecord::Base
|
1792
|
+
self.table_name = "ads"
|
1793
|
+
declare_schema do
|
1794
|
+
string :title, limit: 250, null: true
|
1795
|
+
text :body, null: true
|
1796
|
+
end
|
623
1797
|
end
|
624
|
-
end
|
625
|
-
|
626
|
-
Advert.connection.schema_cache.clear!
|
627
|
-
Advert.reset_column_information
|
628
1798
|
|
629
|
-
|
630
|
-
|
631
|
-
rename_table :adverts, :ads
|
632
|
-
|
633
|
-
add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
|
634
|
-
add_column :ads, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
1799
|
+
Advert.connection.schema_cache.clear!
|
1800
|
+
Advert.reset_column_information
|
635
1801
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
1802
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")).to(
|
1803
|
+
migrate_up(<<~EOS.strip)
|
1804
|
+
rename_table :adverts, :ads
|
1805
|
+
|
1806
|
+
add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
|
1807
|
+
add_column :ads, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
1808
|
+
|
1809
|
+
#{if defined?(SQLite3)
|
1810
|
+
"add_index :ads, [:id], unique: true, name: 'PRIMARY'\n"
|
1811
|
+
elsif defined?(Mysql2)
|
1812
|
+
"execute \"ALTER TABLE ads DROP PRIMARY KEY, ADD PRIMARY KEY (id)\"\n\n" +
|
1813
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
|
1814
|
+
"add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")"
|
1815
|
+
end}
|
1816
|
+
EOS
|
1817
|
+
.and migrate_down(<<~EOS.strip)
|
1818
|
+
remove_column :ads, :title
|
1819
|
+
remove_column :ads, :body
|
1820
|
+
|
1821
|
+
rename_table :ads, :adverts
|
1822
|
+
|
1823
|
+
#{if defined?(SQLite3)
|
1824
|
+
"add_index :adverts, [:id], unique: true, name: 'PRIMARY'\n"
|
1825
|
+
elsif defined?(Mysql2)
|
1826
|
+
"execute \"ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (id)\"\n\n" +
|
1827
|
+
"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" +
|
1828
|
+
"remove_foreign_key(\"adverts\", name: \"on_c_id\")"
|
1829
|
+
end}
|
657
1830
|
EOS
|
658
|
-
|
1831
|
+
)
|
659
1832
|
|
660
|
-
|
1833
|
+
# Set the table name back to what it should be and confirm we're in sync:
|
661
1834
|
|
662
|
-
|
1835
|
+
nuke_model_class(Advert)
|
663
1836
|
|
664
|
-
|
665
|
-
|
666
|
-
|
1837
|
+
class Advert < ActiveRecord::Base
|
1838
|
+
self.table_name = "adverts"
|
1839
|
+
end
|
667
1840
|
|
668
|
-
|
1841
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
|
669
1842
|
|
670
|
-
|
1843
|
+
### Rename a table
|
671
1844
|
|
672
|
-
|
1845
|
+
# 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.
|
673
1846
|
|
674
|
-
|
1847
|
+
nuke_model_class(Advert)
|
675
1848
|
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
1849
|
+
class Advertisement < ActiveRecord::Base
|
1850
|
+
declare_schema do
|
1851
|
+
string :title, limit: 250, null: true
|
1852
|
+
text :body, null: true
|
1853
|
+
end
|
680
1854
|
end
|
681
|
-
end
|
682
|
-
|
683
|
-
expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")).to(
|
684
|
-
migrate_up(<<~EOS.strip)
|
685
|
-
rename_table :adverts, :advertisements
|
686
|
-
|
687
|
-
add_column :advertisements, :title, :string, limit: 250, null: true#{charset_and_collation}
|
688
|
-
add_column :advertisements, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
689
|
-
remove_column :advertisements, :name
|
690
1855
|
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
1856
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")).to(
|
1857
|
+
migrate_up(<<~EOS.strip)
|
1858
|
+
rename_table :adverts, :advertisements
|
1859
|
+
|
1860
|
+
add_column :advertisements, :title, :string, limit: 250, null: true#{charset_and_collation}
|
1861
|
+
add_column :advertisements, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
1862
|
+
remove_column :advertisements, :name
|
1863
|
+
|
1864
|
+
#{if defined?(SQLite3)
|
1865
|
+
"add_index :advertisements, [:id], unique: true, name: 'PRIMARY'"
|
1866
|
+
elsif defined?(Mysql2)
|
1867
|
+
"execute \"ALTER TABLE advertisements DROP PRIMARY KEY, ADD PRIMARY KEY (id)\""
|
1868
|
+
end}
|
1869
|
+
EOS
|
1870
|
+
.and migrate_down(<<~EOS.strip)
|
1871
|
+
remove_column :advertisements, :title
|
1872
|
+
remove_column :advertisements, :body
|
1873
|
+
add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
|
1874
|
+
|
1875
|
+
rename_table :advertisements, :adverts
|
1876
|
+
|
1877
|
+
#{if defined?(SQLite3)
|
1878
|
+
"add_index :adverts, [:id], unique: true, name: 'PRIMARY'"
|
1879
|
+
elsif defined?(Mysql2)
|
1880
|
+
"execute \"ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (id)\""
|
1881
|
+
end}
|
696
1882
|
EOS
|
697
|
-
|
698
|
-
remove_column :advertisements, :title
|
699
|
-
remove_column :advertisements, :body
|
700
|
-
add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
|
701
|
-
|
702
|
-
rename_table :advertisements, :adverts
|
703
|
-
|
704
|
-
#{if defined?(SQLite3)
|
705
|
-
"add_index :adverts, [:id], unique: true, name: 'PRIMARY'"
|
706
|
-
elsif defined?(Mysql2)
|
707
|
-
"execute \"ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (id)\""
|
708
|
-
end}
|
709
|
-
EOS
|
710
|
-
)
|
1883
|
+
)
|
711
1884
|
|
712
|
-
|
1885
|
+
### Drop a table
|
713
1886
|
|
714
|
-
|
1887
|
+
nuke_model_class(Advertisement)
|
715
1888
|
|
716
|
-
|
1889
|
+
# If you delete a model, the migration generator will create a `drop_table` migration.
|
717
1890
|
|
718
|
-
|
1891
|
+
# Dropping tables is where the automatic down-migration really comes in handy:
|
719
1892
|
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
1893
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
1894
|
+
migrate_up(<<~EOS.strip)
|
1895
|
+
drop_table :adverts
|
1896
|
+
EOS
|
1897
|
+
.and migrate_down(<<~EOS.strip)
|
1898
|
+
create_table "adverts"#{table_options}, force: :cascade do |t|
|
1899
|
+
t.string "name", limit: 250#{charset_and_collation}
|
1900
|
+
end
|
728
1901
|
EOS
|
729
|
-
|
1902
|
+
)
|
730
1903
|
|
731
|
-
|
1904
|
+
## STI
|
732
1905
|
|
733
|
-
|
1906
|
+
### Adding an STI subclass
|
734
1907
|
|
735
|
-
|
1908
|
+
# Adding a subclass or two should introduce the 'type' column and no other changes
|
736
1909
|
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
1910
|
+
class Advert < ActiveRecord::Base
|
1911
|
+
declare_schema do
|
1912
|
+
text :body, null: true
|
1913
|
+
string :title, default: "Untitled", limit: 250, null: true
|
1914
|
+
end
|
741
1915
|
end
|
742
|
-
|
743
|
-
|
744
|
-
ActiveRecord::Migration.class_eval(up)
|
1916
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
1917
|
+
ActiveRecord::Migration.class_eval(up)
|
745
1918
|
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
1919
|
+
class FancyAdvert < Advert
|
1920
|
+
end
|
1921
|
+
class SuperFancyAdvert < FancyAdvert
|
1922
|
+
end
|
750
1923
|
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
1924
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run do |migrations|
|
1925
|
+
expect(migrations).to(
|
1926
|
+
migrate_up(<<~EOS.strip)
|
1927
|
+
add_column :adverts, :type, :string, limit: 250, null: true#{charset_and_collation}
|
755
1928
|
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
1929
|
+
add_index :adverts, [:type], name: 'on_type'
|
1930
|
+
EOS
|
1931
|
+
.and migrate_down(<<~EOS.strip)
|
1932
|
+
remove_column :adverts, :type
|
760
1933
|
|
761
|
-
|
1934
|
+
remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid
|
762
1935
|
EOS
|
763
|
-
|
764
|
-
|
1936
|
+
)
|
1937
|
+
end
|
765
1938
|
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
1939
|
+
Advert.field_specs.delete(:type)
|
1940
|
+
nuke_model_class(SuperFancyAdvert)
|
1941
|
+
nuke_model_class(FancyAdvert)
|
1942
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["type"] }
|
770
1943
|
|
771
|
-
|
1944
|
+
## Coping with multiple changes
|
772
1945
|
|
773
|
-
|
1946
|
+
# The migration generator is designed to create complete migrations even if many changes to the models have taken place.
|
774
1947
|
|
775
|
-
|
1948
|
+
# First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
|
776
1949
|
|
777
|
-
|
778
|
-
|
779
|
-
|
1950
|
+
ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
|
1951
|
+
Advert.connection.schema_cache.clear!
|
1952
|
+
Advert.reset_column_information
|
780
1953
|
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
1954
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
1955
|
+
to eq(["adverts"])
|
1956
|
+
expect(Advert.columns.map(&:name).sort).to eq(["body", "id", "title"])
|
1957
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
|
785
1958
|
|
786
1959
|
|
787
|
-
|
1960
|
+
### Rename a column and change the default
|
788
1961
|
|
789
|
-
|
1962
|
+
Advert.field_specs.clear
|
790
1963
|
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
1964
|
+
class Advert < ActiveRecord::Base
|
1965
|
+
declare_schema do
|
1966
|
+
string :name, default: "No Name", limit: 250, null: true
|
1967
|
+
text :body, null: true
|
1968
|
+
end
|
795
1969
|
end
|
796
|
-
end
|
797
1970
|
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
1971
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })).to(
|
1972
|
+
migrate_up(<<~EOS.strip)
|
1973
|
+
rename_column :adverts, :title, :name
|
1974
|
+
change_column :adverts, :name, :string, limit: 250, null: true, default: "No Name"#{charset_and_collation}
|
1975
|
+
EOS
|
1976
|
+
.and migrate_down(<<~EOS.strip)
|
1977
|
+
rename_column :adverts, :name, :title
|
1978
|
+
change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
|
806
1979
|
EOS
|
807
|
-
|
1980
|
+
)
|
808
1981
|
|
809
|
-
|
1982
|
+
### Rename a table and add a column
|
810
1983
|
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
1984
|
+
nuke_model_class(Advert)
|
1985
|
+
class Ad < ActiveRecord::Base
|
1986
|
+
declare_schema do
|
1987
|
+
string :title, default: "Untitled", limit: 250
|
1988
|
+
text :body, null: true
|
1989
|
+
datetime :created_at
|
1990
|
+
end
|
817
1991
|
end
|
818
|
-
end
|
819
1992
|
|
820
|
-
|
821
|
-
|
822
|
-
|
1993
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)).to(
|
1994
|
+
migrate_up(<<~EOS.strip)
|
1995
|
+
rename_table :adverts, :ads
|
823
1996
|
|
824
|
-
|
825
|
-
|
1997
|
+
add_column :ads, :created_at, :datetime, null: false
|
1998
|
+
change_column :ads, :title, :string, limit: 250, null: false, default: \"Untitled\"#{charset_and_collation}
|
826
1999
|
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
2000
|
+
#{if defined?(SQLite3)
|
2001
|
+
"add_index :ads, [:id], unique: true, name: 'PRIMARY'"
|
2002
|
+
elsif defined?(Mysql2)
|
2003
|
+
'execute "ALTER TABLE ads DROP PRIMARY KEY, ADD PRIMARY KEY (id)"'
|
2004
|
+
end}
|
832
2005
|
EOS
|
833
|
-
|
2006
|
+
)
|
834
2007
|
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
2008
|
+
class Advert < ActiveRecord::Base
|
2009
|
+
declare_schema do
|
2010
|
+
text :body, null: true
|
2011
|
+
string :title, default: "Untitled", limit: 250, null: true
|
2012
|
+
end
|
839
2013
|
end
|
840
|
-
end
|
841
2014
|
|
842
|
-
|
2015
|
+
## Legacy Keys
|
843
2016
|
|
844
|
-
|
2017
|
+
# DeclareSchema has some support for legacy keys.
|
845
2018
|
|
846
|
-
|
2019
|
+
nuke_model_class(Ad)
|
847
2020
|
|
848
|
-
|
849
|
-
|
850
|
-
|
2021
|
+
class Advert < ActiveRecord::Base
|
2022
|
+
declare_schema do
|
2023
|
+
text :body, null: true
|
2024
|
+
end
|
2025
|
+
self.primary_key = "advert_id"
|
851
2026
|
end
|
852
|
-
self.primary_key = "advert_id"
|
853
|
-
end
|
854
2027
|
|
855
|
-
|
856
|
-
|
857
|
-
|
2028
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })).to(
|
2029
|
+
migrate_up(<<~EOS.strip)
|
2030
|
+
rename_column :adverts, :id, :advert_id
|
858
2031
|
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
2032
|
+
#{if defined?(SQLite3)
|
2033
|
+
"add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY'"
|
2034
|
+
elsif defined?(Mysql2)
|
2035
|
+
'execute "ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (advert_id)"'
|
2036
|
+
end}
|
864
2037
|
EOS
|
865
|
-
|
2038
|
+
)
|
866
2039
|
|
867
|
-
|
868
|
-
|
2040
|
+
nuke_model_class(Advert)
|
2041
|
+
ActiveRecord::Base.connection.execute("drop table `adverts`;")
|
869
2042
|
|
870
|
-
|
2043
|
+
## DSL
|
871
2044
|
|
872
|
-
|
2045
|
+
# The DSL allows lambdas and constants
|
873
2046
|
|
874
|
-
|
875
|
-
|
876
|
-
|
2047
|
+
class User < ActiveRecord::Base
|
2048
|
+
declare_schema do
|
2049
|
+
string :company, limit: 250, ruby_default: -> { "BigCorp" }
|
2050
|
+
end
|
877
2051
|
end
|
878
|
-
|
879
|
-
|
880
|
-
expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
|
2052
|
+
expect(User.field_specs.keys).to eq(['company'])
|
2053
|
+
expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
|
881
2054
|
|
882
|
-
|
2055
|
+
## validates
|
883
2056
|
|
884
|
-
|
2057
|
+
# DeclareSchema can accept a validates hash in the field options.
|
885
2058
|
|
886
|
-
|
887
|
-
|
888
|
-
|
2059
|
+
class Ad < ActiveRecord::Base
|
2060
|
+
class << self
|
2061
|
+
def validates(field_name, options)
|
2062
|
+
end
|
889
2063
|
end
|
890
2064
|
end
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
2065
|
+
expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
|
2066
|
+
class Ad < ActiveRecord::Base
|
2067
|
+
declare_schema do
|
2068
|
+
string :company, limit: 250, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
|
2069
|
+
end
|
2070
|
+
self.primary_key = "advert_id"
|
896
2071
|
end
|
897
|
-
|
2072
|
+
up, _down = Generators::DeclareSchema::Migration::Migrator.run
|
2073
|
+
ActiveRecord::Migration.class_eval(up)
|
2074
|
+
expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
|
898
2075
|
end
|
899
|
-
up, _down = Generators::DeclareSchema::Migration::Migrator.run
|
900
|
-
ActiveRecord::Migration.class_eval(up)
|
901
|
-
expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
|
902
|
-
end
|
903
2076
|
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
2077
|
+
describe 'serialize' do
|
2078
|
+
before do
|
2079
|
+
class Ad < ActiveRecord::Base
|
2080
|
+
@serialize_args = []
|
908
2081
|
|
909
|
-
|
910
|
-
|
2082
|
+
class << self
|
2083
|
+
attr_reader :serialize_args
|
911
2084
|
|
912
|
-
|
913
|
-
|
2085
|
+
def serialize(*args)
|
2086
|
+
@serialize_args << args
|
2087
|
+
end
|
914
2088
|
end
|
915
2089
|
end
|
916
2090
|
end
|
917
|
-
end
|
918
2091
|
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
2092
|
+
describe 'untyped' do
|
2093
|
+
it 'allows serialize: true' do
|
2094
|
+
class Ad < ActiveRecord::Base
|
2095
|
+
declare_schema do
|
2096
|
+
text :allow_list, limit: 0xFFFF, serialize: true
|
2097
|
+
end
|
924
2098
|
end
|
925
|
-
end
|
926
2099
|
|
927
|
-
|
928
|
-
|
2100
|
+
expect(Ad.serialize_args).to eq([[:allow_list]])
|
2101
|
+
end
|
929
2102
|
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
2103
|
+
it 'converts defaults with .to_yaml' do
|
2104
|
+
class Ad < ActiveRecord::Base
|
2105
|
+
declare_schema do
|
2106
|
+
string :allow_list, limit: 250, serialize: true, null: true, default: []
|
2107
|
+
string :allow_hash, limit: 250, serialize: true, null: true, default: {}
|
2108
|
+
string :allow_string, limit: 250, serialize: true, null: true, default: ['abc']
|
2109
|
+
string :allow_null, limit: 250, serialize: true, null: true, default: nil
|
2110
|
+
end
|
937
2111
|
end
|
938
|
-
end
|
939
2112
|
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
2113
|
+
expect(Ad.field_specs['allow_list'].default).to eq("--- []\n")
|
2114
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("--- {}\n")
|
2115
|
+
expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
|
2116
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
2117
|
+
end
|
944
2118
|
end
|
945
|
-
end
|
946
2119
|
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
2120
|
+
describe 'Array' do
|
2121
|
+
it 'allows serialize: Array' do
|
2122
|
+
class Ad < ActiveRecord::Base
|
2123
|
+
declare_schema do
|
2124
|
+
string :allow_list, limit: 250, serialize: Array, null: true
|
2125
|
+
end
|
952
2126
|
end
|
953
|
-
end
|
954
2127
|
|
955
|
-
|
956
|
-
|
2128
|
+
expect(Ad.serialize_args).to eq([[:allow_list, Array]])
|
2129
|
+
end
|
957
2130
|
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
2131
|
+
it 'allows Array defaults' do
|
2132
|
+
class Ad < ActiveRecord::Base
|
2133
|
+
declare_schema do
|
2134
|
+
string :allow_list, limit: 250, serialize: Array, null: true, default: [2]
|
2135
|
+
string :allow_string, limit: 250, serialize: Array, null: true, default: ['abc']
|
2136
|
+
string :allow_empty, limit: 250, serialize: Array, null: true, default: []
|
2137
|
+
string :allow_null, limit: 250, serialize: Array, null: true, default: nil
|
2138
|
+
end
|
965
2139
|
end
|
966
|
-
end
|
967
2140
|
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
2141
|
+
expect(Ad.field_specs['allow_list'].default).to eq("---\n- 2\n")
|
2142
|
+
expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
|
2143
|
+
expect(Ad.field_specs['allow_empty'].default).to eq(nil)
|
2144
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
2145
|
+
end
|
972
2146
|
end
|
973
|
-
end
|
974
2147
|
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
2148
|
+
describe 'Hash' do
|
2149
|
+
it 'allows serialize: Hash' do
|
2150
|
+
class Ad < ActiveRecord::Base
|
2151
|
+
declare_schema do
|
2152
|
+
string :allow_list, limit: 250, serialize: Hash, null: true
|
2153
|
+
end
|
980
2154
|
end
|
981
|
-
end
|
982
2155
|
|
983
|
-
|
984
|
-
|
2156
|
+
expect(Ad.serialize_args).to eq([[:allow_list, Hash]])
|
2157
|
+
end
|
985
2158
|
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
2159
|
+
it 'allows Hash defaults' do
|
2160
|
+
class Ad < ActiveRecord::Base
|
2161
|
+
declare_schema do
|
2162
|
+
string :allow_loc, limit: 250, serialize: Hash, null: true, default: { 'state' => 'CA' }
|
2163
|
+
string :allow_hash, limit: 250, serialize: Hash, null: true, default: {}
|
2164
|
+
string :allow_null, limit: 250, serialize: Hash, null: true, default: nil
|
2165
|
+
end
|
992
2166
|
end
|
993
|
-
end
|
994
2167
|
|
995
|
-
|
996
|
-
|
997
|
-
|
2168
|
+
expect(Ad.field_specs['allow_loc'].default).to eq("---\nstate: CA\n")
|
2169
|
+
expect(Ad.field_specs['allow_hash'].default).to eq(nil)
|
2170
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
2171
|
+
end
|
998
2172
|
end
|
999
|
-
end
|
1000
2173
|
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
2174
|
+
describe 'JSON' do
|
2175
|
+
it 'allows serialize: JSON' do
|
2176
|
+
class Ad < ActiveRecord::Base
|
2177
|
+
declare_schema do
|
2178
|
+
string :allow_list, limit: 250, serialize: JSON
|
2179
|
+
end
|
1006
2180
|
end
|
1007
|
-
end
|
1008
2181
|
|
1009
|
-
|
1010
|
-
|
2182
|
+
expect(Ad.serialize_args).to eq([[:allow_list, JSON]])
|
2183
|
+
end
|
1011
2184
|
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
2185
|
+
it 'allows JSON defaults' do
|
2186
|
+
class Ad < ActiveRecord::Base
|
2187
|
+
declare_schema do
|
2188
|
+
string :allow_hash, limit: 250, serialize: JSON, null: true, default: { 'state' => 'CA' }
|
2189
|
+
string :allow_empty_array, limit: 250, serialize: JSON, null: true, default: []
|
2190
|
+
string :allow_empty_hash, limit: 250, serialize: JSON, null: true, default: {}
|
2191
|
+
string :allow_null, limit: 250, serialize: JSON, null: true, default: nil
|
2192
|
+
end
|
1019
2193
|
end
|
1020
|
-
end
|
1021
2194
|
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
2195
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("{\"state\":\"CA\"}")
|
2196
|
+
expect(Ad.field_specs['allow_empty_array'].default).to eq("[]")
|
2197
|
+
expect(Ad.field_specs['allow_empty_hash'].default).to eq("{}")
|
2198
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
2199
|
+
end
|
1026
2200
|
end
|
1027
|
-
end
|
1028
2201
|
|
1029
|
-
|
1030
|
-
|
2202
|
+
class ValueClass
|
2203
|
+
delegate :present?, :inspect, to: :@value
|
1031
2204
|
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
2205
|
+
def initialize(value)
|
2206
|
+
@value = value
|
2207
|
+
end
|
1035
2208
|
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
2209
|
+
class << self
|
2210
|
+
def dump(object)
|
2211
|
+
if object&.present?
|
2212
|
+
object.inspect
|
2213
|
+
end
|
1040
2214
|
end
|
1041
|
-
end
|
1042
2215
|
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
2216
|
+
def load(serialized)
|
2217
|
+
if serialized
|
2218
|
+
raise 'not used ???'
|
2219
|
+
end
|
1046
2220
|
end
|
1047
2221
|
end
|
1048
2222
|
end
|
1049
|
-
end
|
1050
2223
|
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
2224
|
+
describe 'custom coder' do
|
2225
|
+
it 'allows serialize: ValueClass' do
|
2226
|
+
class Ad < ActiveRecord::Base
|
2227
|
+
declare_schema do
|
2228
|
+
string :allow_list, limit: 250, serialize: ValueClass
|
2229
|
+
end
|
1056
2230
|
end
|
1057
|
-
end
|
1058
2231
|
|
1059
|
-
|
1060
|
-
|
2232
|
+
expect(Ad.serialize_args).to eq([[:allow_list, ValueClass]])
|
2233
|
+
end
|
1061
2234
|
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
2235
|
+
it 'allows ValueClass defaults' do
|
2236
|
+
class Ad < ActiveRecord::Base
|
2237
|
+
declare_schema do
|
2238
|
+
string :allow_hash, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([2])
|
2239
|
+
string :allow_empty_array, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([])
|
2240
|
+
string :allow_null, limit: 250, serialize: ValueClass, null: true, default: nil
|
2241
|
+
end
|
1068
2242
|
end
|
1069
|
-
end
|
1070
2243
|
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
2244
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("[2]")
|
2245
|
+
expect(Ad.field_specs['allow_empty_array'].default).to eq(nil)
|
2246
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
2247
|
+
end
|
1074
2248
|
end
|
1075
|
-
end
|
1076
2249
|
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
2250
|
+
it 'disallows serialize: with a non-string column type' do
|
2251
|
+
expect do
|
2252
|
+
class Ad < ActiveRecord::Base
|
2253
|
+
declare_schema do
|
2254
|
+
integer :allow_list, limit: 8, serialize: true
|
2255
|
+
end
|
1082
2256
|
end
|
1083
|
-
end
|
1084
|
-
end
|
1085
|
-
end
|
1086
|
-
end
|
1087
|
-
|
1088
|
-
context "for Rails #{Rails::VERSION::MAJOR}" do
|
1089
|
-
if Rails::VERSION::MAJOR >= 5
|
1090
|
-
let(:optional_true) { { optional: true } }
|
1091
|
-
let(:optional_false) { { optional: false } }
|
1092
|
-
else
|
1093
|
-
let(:optional_true) { {} }
|
1094
|
-
let(:optional_false) { {} }
|
2257
|
+
end.to raise_exception(ArgumentError, /must be :string or :text/)
|
2258
|
+
end
|
1095
2259
|
end
|
1096
|
-
let(:optional_flag) { { false => optional_false, true => optional_true } }
|
1097
2260
|
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
2261
|
+
context "for Rails #{Rails::VERSION::MAJOR}" do
|
2262
|
+
if Rails::VERSION::MAJOR >= 5
|
2263
|
+
let(:optional_true) { { optional: true } }
|
2264
|
+
let(:optional_false) { { optional: false } }
|
2265
|
+
else
|
2266
|
+
let(:optional_true) { {} }
|
2267
|
+
let(:optional_false) { {} }
|
2268
|
+
end
|
2269
|
+
let(:optional_flag) { { false => optional_false, true => optional_true } }
|
2270
|
+
|
2271
|
+
describe 'belongs_to' do
|
2272
|
+
before do
|
2273
|
+
unless defined?(AdCategory)
|
2274
|
+
class AdCategory < ActiveRecord::Base
|
2275
|
+
declare_schema { }
|
2276
|
+
end
|
1103
2277
|
end
|
1104
|
-
end
|
1105
2278
|
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
2279
|
+
class Advert < ActiveRecord::Base
|
2280
|
+
declare_schema do
|
2281
|
+
string :name, limit: 250, null: true
|
2282
|
+
integer :category_id, limit: 8
|
2283
|
+
integer :nullable_category_id, limit: 8, null: true
|
2284
|
+
end
|
1111
2285
|
end
|
2286
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
2287
|
+
ActiveRecord::Migration.class_eval(up)
|
1112
2288
|
end
|
1113
|
-
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
1114
|
-
ActiveRecord::Migration.class_eval(up)
|
1115
|
-
end
|
1116
|
-
|
1117
|
-
it 'passes through optional: when given' do
|
1118
|
-
class AdvertBelongsTo < ActiveRecord::Base
|
1119
|
-
self.table_name = 'adverts'
|
1120
|
-
fields { }
|
1121
|
-
reset_column_information
|
1122
|
-
belongs_to :ad_category, optional: true
|
1123
|
-
end
|
1124
|
-
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
|
1125
|
-
end
|
1126
2289
|
|
1127
|
-
|
1128
|
-
it 'passes through optional: true, null: false' do
|
2290
|
+
it 'passes through optional: when given' do
|
1129
2291
|
class AdvertBelongsTo < ActiveRecord::Base
|
1130
2292
|
self.table_name = 'adverts'
|
1131
|
-
|
2293
|
+
declare_schema { }
|
1132
2294
|
reset_column_information
|
1133
|
-
belongs_to :ad_category, optional: true
|
2295
|
+
belongs_to :ad_category, optional: true
|
1134
2296
|
end
|
1135
2297
|
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
|
1136
|
-
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(false)
|
1137
2298
|
end
|
1138
2299
|
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
2300
|
+
describe 'contradictory settings' do # contradictory settings are ok--for example, during migration
|
2301
|
+
it 'passes through optional: true, null: false' do
|
2302
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
2303
|
+
self.table_name = 'adverts'
|
2304
|
+
declare_schema { }
|
2305
|
+
reset_column_information
|
2306
|
+
belongs_to :ad_category, optional: true, null: false
|
2307
|
+
end
|
2308
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
|
2309
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(false)
|
1145
2310
|
end
|
1146
|
-
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_false)
|
1147
|
-
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(true)
|
1148
|
-
end
|
1149
|
-
end
|
1150
2311
|
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
|
1161
|
-
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
2312
|
+
it 'passes through optional: false, null: true' do
|
2313
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
2314
|
+
self.table_name = 'adverts'
|
2315
|
+
declare_schema { }
|
2316
|
+
reset_column_information
|
2317
|
+
belongs_to :ad_category, optional: false, null: true
|
2318
|
+
end
|
2319
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_false)
|
2320
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(true)
|
1162
2321
|
end
|
2322
|
+
end
|
1163
2323
|
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
2324
|
+
[false, true].each do |nullable|
|
2325
|
+
context "nullable=#{nullable}" do
|
2326
|
+
it 'infers optional: from null:' do
|
2327
|
+
eval <<~EOS
|
2328
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
2329
|
+
declare_schema { }
|
2330
|
+
belongs_to :ad_category, null: #{nullable}
|
2331
|
+
end
|
2332
|
+
EOS
|
2333
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
|
2334
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
2335
|
+
end
|
2336
|
+
|
2337
|
+
it 'infers null: from optional:' do
|
2338
|
+
eval <<~EOS
|
2339
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
2340
|
+
declare_schema { }
|
2341
|
+
belongs_to :ad_category, optional: #{nullable}
|
2342
|
+
end
|
2343
|
+
EOS
|
2344
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
|
2345
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
2346
|
+
end
|
1173
2347
|
end
|
1174
2348
|
end
|
1175
2349
|
end
|
1176
2350
|
end
|
1177
|
-
end
|
1178
2351
|
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
2352
|
+
describe 'migration base class' do
|
2353
|
+
it 'adapts to Rails 4' do
|
2354
|
+
class Advert < active_record_base_class.constantize
|
2355
|
+
declare_schema do
|
2356
|
+
string :title, limit: 100
|
2357
|
+
end
|
1184
2358
|
end
|
1185
|
-
end
|
1186
2359
|
|
1187
|
-
|
2360
|
+
generate_migrations '-n', '-m'
|
1188
2361
|
|
1189
|
-
|
1190
|
-
|
2362
|
+
migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
|
2363
|
+
expect(migrations.size).to eq(1), migrations.inspect
|
1191
2364
|
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
2365
|
+
migration_content = File.read(migrations.first)
|
2366
|
+
first_line = migration_content.split("\n").first
|
2367
|
+
base_class = first_line.split(' < ').last
|
2368
|
+
expect(base_class).to eq("(Rails::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration)")
|
2369
|
+
end
|
1196
2370
|
end
|
1197
|
-
end
|
1198
2371
|
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
2372
|
+
context 'Does not generate migrations' do
|
2373
|
+
it 'for aliased fields bigint -> integer limit 8' do
|
2374
|
+
if Rails::VERSION::MAJOR >= 5 || !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
2375
|
+
class Advert < active_record_base_class.constantize
|
2376
|
+
declare_schema do
|
2377
|
+
bigint :price
|
2378
|
+
end
|
1205
2379
|
end
|
1206
|
-
end
|
1207
2380
|
|
1208
|
-
|
2381
|
+
generate_migrations '-n', '-m'
|
1209
2382
|
|
1210
|
-
|
1211
|
-
|
2383
|
+
migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
|
2384
|
+
expect(migrations.size).to eq(1), migrations.inspect
|
1212
2385
|
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
2386
|
+
if defined?(Mysql2) && Rails::VERSION::MAJOR < 5
|
2387
|
+
ActiveRecord::Base.connection.execute("ALTER TABLE adverts ADD PRIMARY KEY (id)")
|
2388
|
+
end
|
1216
2389
|
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
2390
|
+
class Advert < active_record_base_class.constantize
|
2391
|
+
declare_schema do
|
2392
|
+
integer :price, limit: 8
|
2393
|
+
end
|
1220
2394
|
end
|
1221
|
-
end
|
1222
2395
|
|
1223
|
-
|
2396
|
+
expect { generate_migrations '-n', '-g' }.to output("Database and models match -- nothing to change\n").to_stdout
|
2397
|
+
end
|
1224
2398
|
end
|
1225
2399
|
end
|
1226
2400
|
end
|