declare_schema 0.9.0 → 0.11.1

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