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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +1 -1
  3. data/CHANGELOG.md +28 -1
  4. data/Gemfile.lock +1 -1
  5. data/README.md +91 -13
  6. data/lib/declare_schema.rb +46 -0
  7. data/lib/declare_schema/dsl.rb +39 -0
  8. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +23 -4
  9. data/lib/declare_schema/model.rb +51 -59
  10. data/lib/declare_schema/model/field_spec.rb +11 -8
  11. data/lib/declare_schema/model/foreign_key_definition.rb +4 -8
  12. data/lib/declare_schema/model/habtm_model_shim.rb +1 -1
  13. data/lib/declare_schema/model/index_definition.rb +0 -19
  14. data/lib/declare_schema/schema_change/all.rb +22 -0
  15. data/lib/declare_schema/schema_change/base.rb +45 -0
  16. data/lib/declare_schema/schema_change/column_add.rb +27 -0
  17. data/lib/declare_schema/schema_change/column_change.rb +32 -0
  18. data/lib/declare_schema/schema_change/column_remove.rb +20 -0
  19. data/lib/declare_schema/schema_change/column_rename.rb +23 -0
  20. data/lib/declare_schema/schema_change/foreign_key_add.rb +25 -0
  21. data/lib/declare_schema/schema_change/foreign_key_remove.rb +20 -0
  22. data/lib/declare_schema/schema_change/index_add.rb +33 -0
  23. data/lib/declare_schema/schema_change/index_remove.rb +20 -0
  24. data/lib/declare_schema/schema_change/primary_key_change.rb +33 -0
  25. data/lib/declare_schema/schema_change/table_add.rb +37 -0
  26. data/lib/declare_schema/schema_change/table_change.rb +36 -0
  27. data/lib/declare_schema/schema_change/table_remove.rb +22 -0
  28. data/lib/declare_schema/schema_change/table_rename.rb +22 -0
  29. data/lib/declare_schema/version.rb +1 -1
  30. data/lib/generators/declare_schema/migration/migrator.rb +189 -202
  31. data/lib/generators/declare_schema/support/model.rb +4 -4
  32. data/spec/lib/declare_schema/api_spec.rb +7 -7
  33. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +41 -15
  34. data/spec/lib/declare_schema/field_spec_spec.rb +50 -6
  35. data/spec/lib/declare_schema/generator_spec.rb +3 -3
  36. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +116 -26
  37. data/spec/lib/declare_schema/migration_generator_spec.rb +1891 -845
  38. data/spec/lib/declare_schema/model/column_spec.rb +47 -17
  39. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +134 -57
  40. data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +3 -3
  41. data/spec/lib/declare_schema/model/index_definition_spec.rb +188 -77
  42. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +75 -11
  43. data/spec/lib/declare_schema/schema_change/base_spec.rb +75 -0
  44. data/spec/lib/declare_schema/schema_change/column_add_spec.rb +30 -0
  45. data/spec/lib/declare_schema/schema_change/column_change_spec.rb +33 -0
  46. data/spec/lib/declare_schema/schema_change/column_remove_spec.rb +30 -0
  47. data/spec/lib/declare_schema/schema_change/column_rename_spec.rb +28 -0
  48. data/spec/lib/declare_schema/schema_change/foreign_key_add_spec.rb +29 -0
  49. data/spec/lib/declare_schema/schema_change/foreign_key_remove_spec.rb +29 -0
  50. data/spec/lib/declare_schema/schema_change/index_add_spec.rb +56 -0
  51. data/spec/lib/declare_schema/schema_change/index_remove_spec.rb +29 -0
  52. data/spec/lib/declare_schema/schema_change/primary_key_change_spec.rb +69 -0
  53. data/spec/lib/declare_schema/schema_change/table_add_spec.rb +50 -0
  54. data/spec/lib/declare_schema/schema_change/table_change_spec.rb +30 -0
  55. data/spec/lib/declare_schema/schema_change/table_remove_spec.rb +27 -0
  56. data/spec/lib/declare_schema/schema_change/table_rename_spec.rb +27 -0
  57. data/spec/lib/declare_schema_spec.rb +101 -0
  58. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +71 -13
  59. data/spec/support/acceptance_spec_helpers.rb +2 -2
  60. metadata +33 -3
  61. 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 << 'fields do'
72
+ buffer << 'declare_schema do'
73
73
  buffer.indent! do
74
74
  field_attributes.each do |attribute|
75
- decl = "%-#{max_attribute_length}s" % attribute.name + ' ' +
76
- attribute.type.to_sym.inspect +
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.name.length }.max
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
- fields do
24
- title :string, limit: 255
25
- body :text
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
- fields do
95
- title :string, :required, limit: 255
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
- fields do
114
- title :string, :unique, limit: 255
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
- before do
7
- load File.expand_path('prepare_testapp.rb', __dir__)
6
+ let(:model) { TestModel.new }
7
+ subject { declared_class.new(model) }
8
8
 
9
- class TestModel < ActiveRecord::Base
10
- fields do
11
- name :string, limit: 127
9
+ context 'Using fields' do
10
+ before do
11
+ load File.expand_path('prepare_testapp.rb', __dir__)
12
12
 
13
- timestamps
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
- let(:model) { TestModel.new }
19
- subject { declared_class.new(model) }
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
- it 'has fields' do
22
- expect(TestModel.field_specs).to be_kind_of(Hash)
23
- expect(TestModel.field_specs.keys).to eq(['name', 'created_at', 'updated_at'])
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
- it 'stores limits' do
28
- expect(TestModel.field_specs['name'].limit).to eq(127), TestModel.field_specs['name'].inspect
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: {}, defined_primary_key: 'id') }
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
- describe 'varbinary' do # TODO: :varbinary is an Invoca addition to Rails; make it a configurable option
89
- it 'is supported' do
90
- subject = described_class.new(model, :binary_dump, :varbinary, limit: 200, null: false, position: 2)
91
- expect(subject.schema_attributes(col_spec)).to eq(type: :varbinary, limit: 200, null: false)
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, :varbinary, :datetime, :date, :time].each do |t|
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
- fields do
15
- one :string, limit: 255
16
- two :integer
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
- it "allows alternate primary keys" do
15
- class Foo < ActiveRecord::Base
16
- fields do
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
- generate_migrations '-n', '-m'
22
- expect(Foo.primary_key).to eq('foo_id')
22
+ generate_migrations '-n', '-m'
23
+ expect(Foo._defined_primary_key).to eq('foo_id')
23
24
 
24
- ### migrate from
25
- # rename from custom primary_key
26
- class Foo < ActiveRecord::Base
27
- fields do
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
- puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
33
- generate_migrations '-n', '-m'
34
- expect(Foo.primary_key).to eq('id')
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
- nuke_model_class(Foo)
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
- ### migrate to
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
- if Rails::VERSION::MAJOR >= 5 && !defined?(Mysql2) # TODO TECH-4814 Put this test back for Mysql2
41
- # replace custom primary_key
81
+ context 'Using declare_schema' do
82
+ it "allows alternate primary keys" do
42
83
  class Foo < ActiveRecord::Base
43
- fields do
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
- ### ensure it doesn't cause further migrations
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
- # check no further migrations
55
- up = Generators::DeclareSchema::Migration::Migrator.run.first
56
- expect(up).to eq("")
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
- # DeclareSchema - Migration Generator
59
- it 'generates migrations' do
60
- ## The migration generator -- introduction
53
+ context 'Using fields' do
54
+ # DeclareSchema - Migration Generator
55
+ it 'generates migrations' do
56
+ ## The migration generator -- introduction
61
57
 
62
- expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
58
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
63
59
 
64
- class Advert < ActiveRecord::Base
65
- end
60
+ class Advert < ActiveRecord::Base
61
+ end
66
62
 
67
- expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
63
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to migrate_up("").and migrate_down("")
68
64
 
69
- Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
65
+ Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
70
66
 
71
- Advert.connection.schema_cache.clear!
72
- Advert.reset_column_information
67
+ Advert.connection.schema_cache.clear!
68
+ Advert.reset_column_information
73
69
 
74
- class Advert < ActiveRecord::Base
75
- fields do
76
- name :string, limit: 250, null: true
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
- up, _ = Generators::DeclareSchema::Migration::Migrator.run.tap do |migrations|
81
- expect(migrations).to(
82
- migrate_up(<<~EOS.strip)
83
- create_table :adverts, id: :bigint do |t|
84
- t.string :name, limit: 250, null: true#{charset_and_collation}
85
- end#{charset_alter_table}
86
- EOS
87
- .and migrate_down("drop_table :adverts")
88
- )
89
- end
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
- ActiveRecord::Migration.class_eval(up)
92
- expect(Advert.columns.map(&:name)).to eq(["id", "name"])
87
+ ActiveRecord::Migration.class_eval(up)
88
+ expect(Advert.columns.map(&:name)).to eq(["id", "name"])
93
89
 
94
- if Rails::VERSION::MAJOR < 5
95
- # Rails 4 drivers don't always create PK properly. Fix that by dropping and recreating.
96
- ActiveRecord::Base.connection.execute("drop table adverts")
97
- if defined?(Mysql2)
98
- ActiveRecord::Base.connection.execute("CREATE TABLE adverts (id integer PRIMARY KEY AUTO_INCREMENT NOT NULL, name varchar(250)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin")
99
- else
100
- ActiveRecord::Base.connection.execute("CREATE TABLE adverts (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, name varchar(250))")
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
- class Advert < ActiveRecord::Base
105
- fields do
106
- name :string, limit: 250, null: true
107
- body :text, null: true
108
- published_at :datetime, null: true
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
- Advert.connection.schema_cache.clear!
113
- Advert.reset_column_information
108
+ Advert.connection.schema_cache.clear!
109
+ Advert.reset_column_information
114
110
 
115
- expect(migrate).to(
116
- migrate_up(<<~EOS.strip)
117
- add_column :adverts, :body, :text#{text_limit}, null: true#{charset_and_collation}
118
- add_column :adverts, :published_at, :datetime, null: true
119
- EOS
120
- .and migrate_down(<<~EOS.strip)
121
- remove_column :adverts, :body
122
- remove_column :adverts, :published_at
123
- EOS
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
- Advert.field_specs.clear # not normally needed
127
- class Advert < ActiveRecord::Base
128
- fields do
129
- name :string, limit: 250, null: true
130
- body :text, null: true
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
- expect(migrate).to(
135
- migrate_up("remove_column :adverts, :published_at").and(
136
- migrate_down("add_column :adverts, :published_at, :datetime#{datetime_precision}, null: true")
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
- nuke_model_class(Advert)
141
- class Advert < ActiveRecord::Base
142
- fields do
143
- title :string, limit: 250, null: true
144
- body :text, null: true
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
149
- migrate_up(<<~EOS.strip)
150
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
151
- remove_column :adverts, :name
152
- EOS
153
- .and migrate_down(<<~EOS.strip)
154
- remove_column :adverts, :title
155
- add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
156
- EOS
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
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })).to(
160
- migrate_up("rename_column :adverts, :name, :title").and(
161
- migrate_down("rename_column :adverts, :title, :name")
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
- migrate
161
+ migrate
166
162
 
167
- class Advert < ActiveRecord::Base
168
- fields do
169
- title :text, null: true
170
- body :text, null: true
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
175
- migrate_up("change_column :adverts, :title, :text#{text_limit}, null: true#{charset_and_collation}").and(
176
- migrate_down("change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}")
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
- class Advert < ActiveRecord::Base
181
- fields do
182
- title :string, default: "Untitled", limit: 250, null: true
183
- body :text, null: true
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
- expect(migrate).to(
188
- migrate_up(<<~EOS.strip)
189
- change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
190
- EOS
191
- .and migrate_down(<<~EOS.strip)
192
- change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
193
- EOS
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
- ### Limits
192
+ ### Limits
197
193
 
198
- class Advert < ActiveRecord::Base
199
- fields do
200
- price :integer, null: true, limit: 2
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
- # Now run the migration, then change the limit:
209
-
210
- ActiveRecord::Migration.class_eval(up)
211
- class Advert < ActiveRecord::Base
212
- fields do
213
- price :integer, null: true, limit: 3
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
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
- ActiveRecord::Migration.class_eval("remove_column :adverts, :price")
227
- class Advert < ActiveRecord::Base
228
- fields do
229
- price :decimal, precision: 4, scale: 1, null: true
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
- # Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
234
- # allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
235
- # If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
236
- # that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
237
-
238
- if defined?(SQLite3)
239
- expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_falsey
240
- end
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
- class Advert < ActiveRecord::Base
243
- fields do
244
- notes :text
245
- description :text, limit: 30000
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
250
- migrate_up(<<~EOS.strip)
251
- add_column :adverts, :price, :decimal, precision: 4, scale: 1, null: true
252
- add_column :adverts, :notes, :text#{text_limit}, null: false#{charset_and_collation}
253
- add_column :adverts, :description, :text#{', limit: 65535' if defined?(Mysql2)}, null: false#{charset_and_collation}
254
- EOS
255
- )
256
-
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
- # In MySQL, limits are applied, rounded up:
262
-
263
- if defined?(Mysql2)
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: 250
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, :notes, :text, limit: 4294967295, null: false#{charset_and_collation}
276
- add_column :adverts, :description, :text, limit: 255, null: false#{charset_and_collation}
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
- # Limits that are too high for MySQL will raise an exception.
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: 0x1_0000_0000
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
- Advert.field_specs.delete :notes
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
- # And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
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
- # To start, we'll set the database schema for `description` to match the above limit of 250.
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
- # Now migrate to an unstated text limit:
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
- description :text
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
- change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
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
- change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
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
- # And migrate to a stated text limit that is the same as the unstated one:
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 do
327
- description :text, limit: 0xffffffff
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
- change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
334
- EOS
335
- .and migrate_down(<<~EOS.strip)
336
- change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
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
- Advert.connection.schema_cache.clear!
354
- Advert.reset_column_information
398
+ Advert.field_specs.delete(:c_id)
399
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
355
400
 
356
- ### Foreign Keys
401
+ # You can avoid generating the index by specifying `index: false`
357
402
 
358
- # DeclareSchema extends the `belongs_to` macro so that it also declares the
359
- # foreign-key field. It also generates an index on the field.
360
-
361
- class Category < ActiveRecord::Base; end
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
370
- migrate_up(<<~EOS.strip)
371
- add_column :adverts, :category_id, :integer, limit: 8, null: false
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
- add_index :adverts, [:category_id], name: 'on_category_id'
417
+ Advert.field_specs.delete(:category_id)
418
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
374
419
 
375
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" if defined?(Mysql2)}
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
- remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid
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
- #{"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" if defined?(Mysql2)}
383
- EOS
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
- Advert.field_specs.delete(:category_id)
387
- Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
437
+ Advert.field_specs.delete(:category_id)
438
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
388
439
 
389
- # If you specify a custom foreign key, the migration generator observes that:
440
+ ### Timestamps and Optimimistic Locking
390
441
 
391
- class Category < ActiveRecord::Base; end
392
- class Advert < ActiveRecord::Base
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
398
- migrate_up(<<~EOS.strip)
399
- add_column :adverts, :c_id, :integer, limit: 8, null: false
445
+ class Advert < ActiveRecord::Base
446
+ fields do
447
+ timestamps
448
+ optimistic_lock
449
+ end
450
+ end
400
451
 
401
- add_index :adverts, [:c_id], name: 'on_c_id'
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
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
404
- "add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
405
- EOS
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
- Advert.field_specs.delete(:c_id)
409
- Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
473
+ ### Indices
410
474
 
411
- # You can avoid generating the index by specifying `index: false`
475
+ # You can add an index to a field definition
412
476
 
413
- class Category < ActiveRecord::Base; end
414
- class Advert < ActiveRecord::Base
415
- fields { }
416
- belongs_to :category, index: false
417
- end
477
+ class Advert < ActiveRecord::Base
478
+ fields do
479
+ title :string, index: true, limit: 250, null: true
480
+ end
481
+ end
418
482
 
419
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
420
- migrate_up(<<~EOS.strip)
421
- add_column :adverts, :category_id, :integer, limit: 8, null: false
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
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
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
- Advert.field_specs.delete(:category_id)
429
- Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
494
+ # You can ask for a unique index
430
495
 
431
- # You can specify the index name with :index
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
- class Category < ActiveRecord::Base; end
434
- class Advert < ActiveRecord::Base
435
- fields { }
436
- belongs_to :category, index: 'my_index'
437
- end
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
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
- add_index :adverts, [:category_id], name: 'my_index'
513
+ # You can specify the name for the index
444
514
 
445
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
446
- "add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
447
- EOS
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
- Advert.field_specs.delete(:category_id)
451
- Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
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
- ### Timestamps and Optimimistic Locking
530
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
454
531
 
455
- # `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
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
- class Advert < ActiveRecord::Base
459
- fields do
460
- timestamps
461
- optimistic_lock
534
+ class Advert < ActiveRecord::Base
535
+ index :title
462
536
  end
463
- end
464
537
 
465
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
466
- migrate_up(<<~EOS.strip)
467
- add_column :adverts, :created_at, :datetime, null: true
468
- add_column :adverts, :updated_at, :datetime, null: true
469
- add_column :adverts, :lock_version, :integer#{lock_version_limit}, null: false, default: 1
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
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
472
- "add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
473
- EOS
474
- .and migrate_down(<<~EOS.strip)
475
- remove_column :adverts, :created_at
476
- remove_column :adverts, :updated_at
477
- remove_column :adverts, :lock_version
549
+ # The available options for the index function are `:unique` and `:name`
478
550
 
479
- #{"remove_foreign_key(\"adverts\", name: \"on_category_id\")\n" +
480
- "remove_foreign_key(\"adverts\", name: \"on_c_id\")" if defined?(Mysql2)}
481
- EOS
482
- )
551
+ class Advert < ActiveRecord::Base
552
+ index :title, unique: true, name: 'my_index'
553
+ end
483
554
 
484
- Advert.field_specs.delete(:updated_at)
485
- Advert.field_specs.delete(:created_at)
486
- Advert.field_specs.delete(:lock_version)
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
- ### Indices
564
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
489
565
 
490
- # You can add an index to a field definition
566
+ # You can create an index on more than one field
491
567
 
492
- class Advert < ActiveRecord::Base
493
- fields do
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
499
- migrate_up(<<~EOS.strip)
500
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
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
- add_index :adverts, [:title], name: 'on_title'
581
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
503
582
 
504
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
505
- "add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
506
- EOS
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
- Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
587
+ ### Rename a table
510
588
 
511
- # You can ask for a unique index
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
- class Advert < ActiveRecord::Base
514
- fields do
515
- title :string, index: true, unique: true, null: true, limit: 250
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
- add_index :adverts, [:title], unique: true, name: 'on_title'
600
+ Advert.connection.schema_cache.clear!
601
+ Advert.reset_column_information
524
602
 
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
- )
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
- Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
624
+ # Set the table name back to what it should be and confirm we're in sync:
531
625
 
532
- # You can specify the name for the index
626
+ nuke_model_class(Advert)
533
627
 
534
- class Advert < ActiveRecord::Base
535
- fields do
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
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
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
- Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
634
+ ### Rename a table
552
635
 
553
- # You can ask for an index outside of the fields block
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
- class Advert < ActiveRecord::Base
556
- index :title
557
- end
638
+ nuke_model_class(Advert)
558
639
 
559
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
560
- migrate_up(<<~EOS.strip)
561
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
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
- add_index :adverts, [:title], name: 'on_title'
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
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
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
- Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
664
+ nuke_model_class(Advertisement)
571
665
 
572
- # The available options for the index function are `:unique` and `:name`
666
+ # If you delete a model, the migration generator will create a `drop_table` migration.
573
667
 
574
- class Advert < ActiveRecord::Base
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
579
- migrate_up(<<~EOS.strip)
580
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
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
- add_index :adverts, [:title], unique: true, name: 'my_index'
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
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
585
- "add_foreign_key(\"adverts\", \"categories\", column: \"c_id\", name: \"on_c_id\")" if defined?(Mysql2)}
586
- EOS
587
- )
1548
+ Advert.field_specs.delete(:category_id)
1549
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
588
1550
 
589
- Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
1551
+ ### Timestamps and Optimimistic Locking
590
1552
 
591
- # You can create an index on more than one field
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
- class Advert < ActiveRecord::Base
594
- index [:title, :category_id]
595
- end
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
598
- migrate_up(<<~EOS.strip)
599
- add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
1580
+ Advert.field_specs.delete(:updated_at)
1581
+ Advert.field_specs.delete(:created_at)
1582
+ Advert.field_specs.delete(:lock_version)
600
1583
 
601
- add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'
1584
+ ### Indices
602
1585
 
603
- #{"add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
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
- Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
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
- # Finally, you can specify that the migration generator should completely ignore an
611
- # index by passing its name to ignore_index in the model.
612
- # This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
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
- ### Rename a table
1603
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
615
1604
 
616
- # 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.
1605
+ # You can ask for a unique index
617
1606
 
618
- class Advert < ActiveRecord::Base
619
- self.table_name = "ads"
620
- fields do
621
- title :string, limit: 250, null: true
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
- Advert.connection.schema_cache.clear!
627
- Advert.reset_column_information
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
- expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")).to(
630
- migrate_up(<<~EOS.strip)
631
- rename_table :adverts, :ads
1622
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
632
1623
 
633
- add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
634
- add_column :ads, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
1624
+ # You can specify the name for the index
635
1625
 
636
- #{if defined?(SQLite3)
637
- "add_index :ads, [:id], unique: true, name: 'PRIMARY'\n"
638
- elsif defined?(Mysql2)
639
- "execute \"ALTER TABLE ads DROP PRIMARY KEY, ADD PRIMARY KEY (id)\"\n\n" +
640
- "add_foreign_key(\"adverts\", \"categories\", column: \"category_id\", name: \"on_category_id\")\n" +
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
- # Set the table name back to what it should be and confirm we're in sync:
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
- nuke_model_class(Advert)
1641
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
663
1642
 
664
- class Advert < ActiveRecord::Base
665
- self.table_name = "adverts"
666
- end
1643
+ # You can ask for an index outside of the fields block
667
1644
 
668
- expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
1645
+ class Advert < ActiveRecord::Base
1646
+ index :title
1647
+ end
669
1648
 
670
- ### Rename a table
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
- # 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.
1658
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
673
1659
 
674
- nuke_model_class(Advert)
1660
+ # The available options for the index function are `:unique` and `:name`
675
1661
 
676
- class Advertisement < ActiveRecord::Base
677
- fields do
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
- expect(Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")).to(
684
- migrate_up(<<~EOS.strip)
685
- rename_table :adverts, :advertisements
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
- add_column :advertisements, :title, :string, limit: 250, null: true#{charset_and_collation}
688
- add_column :advertisements, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
689
- remove_column :advertisements, :name
1675
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
690
1676
 
691
- #{if defined?(SQLite3)
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
- ### Drop a table
1679
+ class Advert < ActiveRecord::Base
1680
+ index [:title, :category_id]
1681
+ end
713
1682
 
714
- nuke_model_class(Advertisement)
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
- # If you delete a model, the migration generator will create a `drop_table` migration.
1692
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
717
1693
 
718
- # Dropping tables is where the automatic down-migration really comes in handy:
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
- expect(Generators::DeclareSchema::Migration::Migrator.run).to(
721
- migrate_up(<<~EOS.strip)
722
- drop_table :adverts
723
- EOS
724
- .and migrate_down(<<~EOS.strip)
725
- create_table "adverts"#{table_options}, force: :cascade do |t|
726
- t.string "name", limit: 250#{charset_and_collation}
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
- EOS
729
- )
1708
+ end
1709
+
1710
+ Advert.connection.schema_cache.clear!
1711
+ Advert.reset_column_information
730
1712
 
731
- ## STI
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
- ### Adding an STI subclass
1734
+ # Set the table name back to what it should be and confirm we're in sync:
734
1735
 
735
- # Adding a subclass or two should introduce the 'type' column and no other changes
1736
+ nuke_model_class(Advert)
736
1737
 
737
- class Advert < ActiveRecord::Base
738
- fields do
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
- class FancyAdvert < Advert
747
- end
748
- class SuperFancyAdvert < FancyAdvert
749
- end
1742
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
750
1743
 
751
- up, _ = Generators::DeclareSchema::Migration::Migrator.run do |migrations|
752
- expect(migrations).to(
753
- migrate_up(<<~EOS.strip)
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
- add_index :adverts, [:type], name: 'on_type'
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
- remove_column :adverts, :type
760
-
761
- remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid
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
- Advert.field_specs.delete(:type)
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
- ## Coping with multiple changes
1774
+ nuke_model_class(Advertisement)
772
1775
 
773
- # The migration generator is designed to create complete migrations even if many changes to the models have taken place.
1776
+ # If you delete a model, the migration generator will create a `drop_table` migration.
774
1777
 
775
- # First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
1778
+ # Dropping tables is where the automatic down-migration really comes in handy:
776
1779
 
777
- ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
778
- Advert.connection.schema_cache.clear!
779
- Advert.reset_column_information
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
- expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
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
- ### Rename a column and change the default
1795
+ # Adding a subclass or two should introduce the 'type' column and no other changes
788
1796
 
789
- Advert.field_specs.clear
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
- class Advert < ActiveRecord::Base
792
- fields do
793
- name :string, default: "No Name", limit: 250, null: true
794
- body :text, null: true
1806
+ class FancyAdvert < Advert
1807
+ end
1808
+ class SuperFancyAdvert < FancyAdvert
795
1809
  end
796
- end
797
1810
 
798
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })).to(
799
- migrate_up(<<~EOS.strip)
800
- rename_column :adverts, :title, :name
801
- change_column :adverts, :name, :string, limit: 250, null: true, default: "No Name"#{charset_and_collation}
802
- EOS
803
- .and migrate_down(<<~EOS.strip)
804
- rename_column :adverts, :name, :title
805
- change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
806
- EOS
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
- ### Rename a table and add a column
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
- nuke_model_class(Advert)
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
- expect(Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)).to(
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
- add_column :ads, :created_at, :datetime, null: false
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
- #{if defined?(SQLite3)
828
- "add_index :ads, [:id], unique: true, name: 'PRIMARY'"
829
- elsif defined?(Mysql2)
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
- class Advert < ActiveRecord::Base
836
- fields do
837
- body :text, null: true
838
- title :string, default: "Untitled", limit: 250, null: true
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
- # DeclareSchema has some support for legacy keys.
1845
+ ### Rename a column and change the default
845
1846
 
846
- nuke_model_class(Ad)
1847
+ Advert.field_specs.clear
847
1848
 
848
- class Advert < ActiveRecord::Base
849
- fields do
850
- body :text, null: true
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
- #{if defined?(SQLite3)
860
- "add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY'"
861
- elsif defined?(Mysql2)
862
- 'execute "ALTER TABLE adverts DROP PRIMARY KEY, ADD PRIMARY KEY (advert_id)"'
863
- end}
864
- EOS
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
- nuke_model_class(Advert)
868
- ActiveRecord::Base.connection.execute("drop table `adverts`;")
1867
+ ### Rename a table and add a column
869
1868
 
870
- ## DSL
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
- # The DSL allows lambdas and constants
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
- class User < ActiveRecord::Base
875
- fields do
876
- company :string, limit: 250, ruby_default: -> { "BigCorp" }
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
- ## validates
1893
+ ## Legacy Keys
1894
+
1895
+ # DeclareSchema has some support for legacy keys.
883
1896
 
884
- # DeclareSchema can accept a validates hash in the field options.
1897
+ nuke_model_class(Ad)
885
1898
 
886
- class Ad < ActiveRecord::Base
887
- class << self
888
- def validates(field_name, options)
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
- describe 'serialize' do
905
- before do
906
- class Ad < ActiveRecord::Base
907
- @serialize_args = []
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
- class << self
910
- attr_reader :serialize_args
1912
+ nuke_model_class(Advert)
1913
+ ActiveRecord::Base.connection.execute("drop table `adverts`;")
911
1914
 
912
- def serialize(*args)
913
- @serialize_args << args
914
- end
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
- end
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
- describe 'untyped' do
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
- expect(Ad.serialize_args).to eq([[:allow_list]])
928
- end
1929
+ # DeclareSchema can accept a validates hash in the field options.
929
1930
 
930
- it 'converts defaults with .to_yaml' do
931
- class Ad < ActiveRecord::Base
932
- fields do
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 'Array' do
948
- it 'allows serialize: Array' do
1949
+ describe 'serialize' do
1950
+ before do
949
1951
  class Ad < ActiveRecord::Base
950
- fields do
951
- allow_list :string, limit: 250, serialize: Array, null: true
952
- end
953
- end
1952
+ @serialize_args = []
954
1953
 
955
- expect(Ad.serialize_args).to eq([[:allow_list, Array]])
956
- end
1954
+ class << self
1955
+ attr_reader :serialize_args
957
1956
 
958
- it 'allows Array defaults' do
959
- class Ad < ActiveRecord::Base
960
- fields do
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
- describe 'Hash' do
976
- it 'allows serialize: Hash' do
977
- class Ad < ActiveRecord::Base
978
- fields do
979
- allow_list :string, limit: 250, serialize: Hash, null: true
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
- expect(Ad.serialize_args).to eq([[:allow_list, Hash]])
984
- end
1972
+ expect(Ad.serialize_args).to eq([[:allow_list]])
1973
+ end
985
1974
 
986
- it 'allows Hash defaults' do
987
- class Ad < ActiveRecord::Base
988
- fields do
989
- allow_loc :string, limit: 250, serialize: Hash, null: true, default: { 'state' => 'CA' }
990
- allow_hash :string, limit: 250, serialize: Hash, null: true, default: {}
991
- allow_null :string, limit: 250, serialize: Hash, null: true, default: nil
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
- expect(Ad.field_specs['allow_loc'].default).to eq("---\nstate: CA\n")
996
- expect(Ad.field_specs['allow_hash'].default).to eq(nil)
997
- expect(Ad.field_specs['allow_null'].default).to eq(nil)
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
- describe 'JSON' do
1002
- it 'allows serialize: JSON' do
1003
- class Ad < ActiveRecord::Base
1004
- fields do
1005
- allow_list :string, limit: 250, serialize: JSON
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
- expect(Ad.serialize_args).to eq([[:allow_list, JSON]])
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
- it 'allows JSON defaults' do
1013
- class Ad < ActiveRecord::Base
1014
- fields do
1015
- allow_hash :string, limit: 250, serialize: JSON, null: true, default: { 'state' => 'CA' }
1016
- allow_empty_array :string, limit: 250, serialize: JSON, null: true, default: []
1017
- allow_empty_hash :string, limit: 250, serialize: JSON, null: true, default: {}
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
- expect(Ad.field_specs['allow_hash'].default).to eq("{\"state\":\"CA\"}")
1023
- expect(Ad.field_specs['allow_empty_array'].default).to eq("[]")
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
- class ValueClass
1030
- delegate :present?, :inspect, to: :@value
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
- def initialize(value)
1033
- @value = value
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
- class << self
1037
- def dump(object)
1038
- if object&.present?
1039
- object.inspect
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
- def load(serialized)
1044
- if serialized
1045
- raise 'not used ???'
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
- describe 'custom coder' do
1052
- it 'allows serialize: ValueClass' do
1053
- class Ad < ActiveRecord::Base
1054
- fields do
1055
- allow_list :string, limit: 250, serialize: ValueClass
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
- expect(Ad.serialize_args).to eq([[:allow_list, ValueClass]])
1060
- end
2081
+ class << self
2082
+ def dump(object)
2083
+ if object&.present?
2084
+ object.inspect
2085
+ end
2086
+ end
1061
2087
 
1062
- it 'allows ValueClass defaults' do
1063
- class Ad < ActiveRecord::Base
1064
- fields do
1065
- allow_hash :string, limit: 250, serialize: ValueClass, null: true, default: ValueClass.new([2])
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
- it 'disallows serialize: with a non-string column type' do
1078
- expect do
1079
- class Ad < ActiveRecord::Base
1080
- fields do
1081
- allow_list :integer, limit: 8, serialize: true
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
- context "for Rails #{Rails::VERSION::MAJOR}" do
1089
- if Rails::VERSION::MAJOR >= 5
1090
- let(:optional_true) { { optional: true } }
1091
- let(:optional_false) { { optional: false } }
1092
- else
1093
- let(:optional_true) { {} }
1094
- let(:optional_false) { {} }
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
- describe 'belongs_to' do
1099
- before do
1100
- unless defined?(AdCategory)
1101
- class AdCategory < ActiveRecord::Base
1102
- fields { }
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
- class Advert < ActiveRecord::Base
1107
- fields do
1108
- name :string, limit: 250, null: true
1109
- category_id :integer, limit: 8
1110
- nullable_category_id :integer, limit: 8, null: true
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
- it 'passes through optional: when given' do
1118
- class AdvertBelongsTo < ActiveRecord::Base
1119
- self.table_name = 'adverts'
1120
- fields { }
1121
- reset_column_information
1122
- belongs_to :ad_category, optional: true
1123
- end
1124
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
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
- describe 'contradictory settings' do # contradictory settings are ok--for example, during migration
1128
- it 'passes through optional: true, null: false' do
1129
- class AdvertBelongsTo < ActiveRecord::Base
1130
- self.table_name = 'adverts'
1131
- fields { }
1132
- reset_column_information
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
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
1136
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(false)
2158
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
2159
+ ActiveRecord::Migration.class_eval(up)
1137
2160
  end
1138
2161
 
1139
- it 'passes through optional: false, null: true' do
2162
+ it 'passes through optional: when given' do
1140
2163
  class AdvertBelongsTo < ActiveRecord::Base
1141
2164
  self.table_name = 'adverts'
1142
- fields { }
2165
+ declare_schema { }
1143
2166
  reset_column_information
1144
- belongs_to :ad_category, optional: false, null: true
2167
+ belongs_to :ad_category, optional: true
1145
2168
  end
1146
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_false)
1147
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(true)
2169
+ expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_true)
1148
2170
  end
1149
- end
1150
2171
 
1151
- [false, true].each do |nullable|
1152
- context "nullable=#{nullable}" do
1153
- it 'infers optional: from null:' do
1154
- eval <<~EOS
1155
- class AdvertBelongsTo < ActiveRecord::Base
1156
- fields { }
1157
- belongs_to :ad_category, null: #{nullable}
1158
- end
1159
- EOS
1160
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
1161
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
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 'infers null: from optional:' do
1165
- eval <<~EOS
1166
- class AdvertBelongsTo < ActiveRecord::Base
1167
- fields { }
1168
- belongs_to :ad_category, optional: #{nullable}
1169
- end
1170
- EOS
1171
- expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional_flag[nullable])
1172
- expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
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
- describe 'migration base class' do
1180
- it 'adapts to Rails 4' do
1181
- class Advert < active_record_base_class.constantize
1182
- fields do
1183
- title :string, limit: 100
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
- context 'Does not generate migrations' do
1200
- it 'for aliased fields bigint -> integer limit 8' do
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
- fields do
1204
- price :bigint
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
- if defined?(Mysql2) && Rails::VERSION::MAJOR < 5
1214
- ActiveRecord::Base.connection.execute("ALTER TABLE adverts ADD PRIMARY KEY (id)")
1215
- end
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
- class Advert < active_record_base_class.constantize
1218
- fields do
1219
- price :integer, limit: 8
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
- expect { generate_migrations '-n', '-g' }.to output("Database and models match -- nothing to change\n").to_stdout
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