declare_schema 0.8.0.pre.6 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
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