declare_schema 0.9.0 → 0.11.1

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