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