declare_schema 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +1 -3
- data/README.md +20 -0
- data/Rakefile +13 -20
- data/gemfiles/rails_4.gemfile +0 -1
- data/gemfiles/rails_5.gemfile +0 -1
- data/gemfiles/rails_6.gemfile +0 -1
- data/lib/declare_schema/model.rb +27 -24
- data/lib/declare_schema/model/field_spec.rb +1 -12
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +20 -14
- data/lib/generators/declare_schema/migration/migrator.rb +50 -31
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
- data/lib/generators/declare_schema/support/eval_template.rb +12 -3
- data/lib/generators/declare_schema/support/model.rb +77 -2
- data/spec/lib/declare_schema/api_spec.rb +125 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
- data/spec/lib/declare_schema/generator_spec.rb +57 -0
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
- data/spec/lib/declare_schema/migration_generator_spec.rb +795 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +31 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +72 -0
- data/spec/spec_helper.rb +26 -0
- metadata +11 -12
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
- data/test/api.rdoctest +0 -136
- data/test/generators.rdoctest +0 -62
- data/test/interactive_primary_key.rdoctest +0 -56
- data/test/migration_generator.rdoctest +0 -846
- data/test/migration_generator_comments.rdoctestDISABLED +0 -74
- data/test/prepare_testapp.rb +0 -15
@@ -9,9 +9,18 @@ module DeclareSchema
|
|
9
9
|
private
|
10
10
|
|
11
11
|
def eval_template(template_name)
|
12
|
-
source
|
13
|
-
|
14
|
-
|
12
|
+
source = File.expand_path(find_in_source_paths(template_name))
|
13
|
+
erb = ERB.new(::File.read(source).force_encoding(Encoding::UTF_8), trim_mode: '>')
|
14
|
+
erb.filename = source
|
15
|
+
begin
|
16
|
+
erb.result(binding)
|
17
|
+
rescue Exception => ex
|
18
|
+
raise ex.class, <<~EOS
|
19
|
+
#{ex.message}
|
20
|
+
#{erb.src}
|
21
|
+
#{ex.backtrace.join("\n ")}
|
22
|
+
EOS
|
23
|
+
end
|
15
24
|
end
|
16
25
|
end
|
17
26
|
end
|
@@ -4,6 +4,39 @@ require_relative './eval_template'
|
|
4
4
|
|
5
5
|
module DeclareSchema
|
6
6
|
module Support
|
7
|
+
class IndentedBuffer
|
8
|
+
def initialize(indent: 0)
|
9
|
+
@string = +""
|
10
|
+
@indent = indent
|
11
|
+
@column = 0
|
12
|
+
@indent_amount = 2
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_string
|
16
|
+
@string
|
17
|
+
end
|
18
|
+
|
19
|
+
def indent!
|
20
|
+
@indent += @indent_amount
|
21
|
+
yield
|
22
|
+
@indent -= @indent_amount
|
23
|
+
end
|
24
|
+
|
25
|
+
def newline!
|
26
|
+
@column = 0
|
27
|
+
@string << "\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
def <<(str)
|
31
|
+
if (difference = @indent - @column) > 0
|
32
|
+
@string << ' ' * difference
|
33
|
+
end
|
34
|
+
@column += difference
|
35
|
+
@string << str
|
36
|
+
newline!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
7
40
|
module Model
|
8
41
|
class << self
|
9
42
|
def included(base)
|
@@ -26,9 +59,51 @@ module DeclareSchema
|
|
26
59
|
|
27
60
|
def inject_declare_schema_code_into_model_file
|
28
61
|
gsub_file(model_path, / # attr_accessible :title, :body\n/m, "")
|
29
|
-
inject_into_class
|
30
|
-
|
62
|
+
inject_into_class(model_path, class_name) do
|
63
|
+
declare_model_fields_and_associations
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def declare_model_fields_and_associations
|
70
|
+
buffer = ::DeclareSchema::Support::IndentedBuffer.new(indent: 2)
|
71
|
+
buffer.newline!
|
72
|
+
buffer << 'fields do'
|
73
|
+
buffer.indent! do
|
74
|
+
field_attributes.each do |attribute|
|
75
|
+
decl = "%-#{max_attribute_length}s" % attribute.name + ' ' +
|
76
|
+
attribute.type.to_sym.inspect +
|
77
|
+
case attribute.type.to_s
|
78
|
+
when 'string'
|
79
|
+
', limit: 255'
|
80
|
+
else
|
81
|
+
''
|
82
|
+
end
|
83
|
+
buffer << decl
|
84
|
+
end
|
85
|
+
if options[:timestamps]
|
86
|
+
buffer.newline!
|
87
|
+
buffer << 'timestamps'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
buffer << 'end'
|
91
|
+
|
92
|
+
if bts.any?
|
93
|
+
buffer.newline!
|
94
|
+
bts.each do |bt|
|
95
|
+
buffer << "belongs_to #{bt.to_sym.inspect}"
|
96
|
+
end
|
31
97
|
end
|
98
|
+
if hms.any?
|
99
|
+
buffer.newline
|
100
|
+
hms.each do |hm|
|
101
|
+
buffer << "has_many #{hm.to_sym.inspect}, dependent: :destroy"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
buffer.newline!
|
105
|
+
|
106
|
+
buffer.to_string
|
32
107
|
end
|
33
108
|
|
34
109
|
protected
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'rails/generators'
|
5
|
+
|
6
|
+
RSpec.describe 'DeclareSchema API' do
|
7
|
+
before do
|
8
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'example models' do
|
12
|
+
it 'generates a model' do
|
13
|
+
expect(system("bundle exec rails generate declare_schema:model advert title:string body:text")).to be_truthy
|
14
|
+
|
15
|
+
# The above will generate the test, fixture and a model file like this:
|
16
|
+
# model_declaration = Rails::Generators.invoke('declare_schema:model', ['advert2', 'title:string', 'body:text'])
|
17
|
+
# expect(model_declaration.first).to eq([["Advert"], nil, "app/models/advert.rb", nil,
|
18
|
+
# [["AdvertTest"], "test/models/advert_test.rb", nil, "test/fixtures/adverts.yml"]])
|
19
|
+
|
20
|
+
expect(File.read("#{TESTAPP_PATH}/app/models/advert.rb")).to eq(<<~EOS)
|
21
|
+
class Advert < #{active_record_base_class}
|
22
|
+
|
23
|
+
fields do
|
24
|
+
title :string, limit: 255
|
25
|
+
body :text
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
EOS
|
30
|
+
system("rm -rf #{TESTAPP_PATH}/app/models/advert2.rb #{TESTAPP_PATH}/test/models/advert2.rb #{TESTAPP_PATH}/test/fixtures/advert2.rb")
|
31
|
+
|
32
|
+
# The migration generator uses this information to create a migration.
|
33
|
+
# The following creates and runs the migration:
|
34
|
+
|
35
|
+
expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
|
36
|
+
|
37
|
+
# We're now ready to start demonstrating the API
|
38
|
+
|
39
|
+
Rails.application.config.autoload_paths += ["#{TESTAPP_PATH}/app/models"]
|
40
|
+
|
41
|
+
$LOAD_PATH << "#{TESTAPP_PATH}/app/models"
|
42
|
+
|
43
|
+
unless Rails::VERSION::MAJOR >= 6
|
44
|
+
# TODO: get this to work on Travis for Rails 6
|
45
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
46
|
+
end
|
47
|
+
|
48
|
+
require 'advert'
|
49
|
+
|
50
|
+
## The Basics
|
51
|
+
|
52
|
+
# The main feature of DeclareSchema, aside from the migration generator, is the ability to declare rich types for your fields. For example, you can declare that a field is an email address, and the field will be automatically validated for correct email address syntax.
|
53
|
+
|
54
|
+
### Field Types
|
55
|
+
|
56
|
+
# Field values are returned as the type you specify.
|
57
|
+
|
58
|
+
Advert.destroy_all
|
59
|
+
|
60
|
+
a = Advert.new(body: "This is the body", id: 1, title: "title")
|
61
|
+
expect(a.body).to eq("This is the body")
|
62
|
+
|
63
|
+
# This also works after a round-trip to the database
|
64
|
+
|
65
|
+
a.save!
|
66
|
+
expect(a.reload.body).to eq("This is the body")
|
67
|
+
|
68
|
+
## Names vs. Classes
|
69
|
+
|
70
|
+
## Model extensions
|
71
|
+
|
72
|
+
# DeclareSchema adds a few features to your models.
|
73
|
+
|
74
|
+
### `Model.attr_type`
|
75
|
+
|
76
|
+
# Returns the type (i.e. class) declared for a given field or attribute
|
77
|
+
|
78
|
+
Advert.connection.schema_cache.clear!
|
79
|
+
Advert.reset_column_information
|
80
|
+
|
81
|
+
expect(Advert.attr_type(:title)).to eq(String)
|
82
|
+
expect(Advert.attr_type(:body)).to eq(String)
|
83
|
+
|
84
|
+
## Field validations
|
85
|
+
|
86
|
+
# DeclareSchema gives you some shorthands for declaring some common validations right in the field declaration
|
87
|
+
|
88
|
+
### Required fields
|
89
|
+
|
90
|
+
# The `:required` argument to a field gives a `validates_presence_of`:
|
91
|
+
|
92
|
+
class AdvertWithRequiredTitle < ActiveRecord::Base
|
93
|
+
self.table_name = 'adverts'
|
94
|
+
|
95
|
+
fields do
|
96
|
+
title :string, :required, limit: 255
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
a = AdvertWithRequiredTitle.new
|
101
|
+
expect(a.valid? || a.errors.full_messages).to eq(["Title can't be blank"])
|
102
|
+
a.id = 2
|
103
|
+
a.body = "hello"
|
104
|
+
a.title = "Jimbo"
|
105
|
+
a.save!
|
106
|
+
|
107
|
+
### Unique fields
|
108
|
+
|
109
|
+
# The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
|
110
|
+
|
111
|
+
class AdvertWithUniqueTitle < ActiveRecord::Base
|
112
|
+
self.table_name = 'adverts'
|
113
|
+
|
114
|
+
fields do
|
115
|
+
title :string, :unique, limit: 255
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
a = AdvertWithUniqueTitle.new :title => "Jimbo", id: 3, body: "hello"
|
120
|
+
expect(a.valid? || a.errors.full_messages).to eq(["Title has already been taken"])
|
121
|
+
a.title = "Sambo"
|
122
|
+
a.save!
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -3,11 +3,15 @@
|
|
3
3
|
require_relative '../../../lib/declare_schema/field_declaration_dsl'
|
4
4
|
|
5
5
|
RSpec.describe DeclareSchema::FieldDeclarationDsl do
|
6
|
-
|
7
|
-
|
8
|
-
name :string, limit: 127
|
6
|
+
before do
|
7
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
9
8
|
|
10
|
-
|
9
|
+
class TestModel < ActiveRecord::Base
|
10
|
+
fields do
|
11
|
+
name :string, limit: 127
|
12
|
+
|
13
|
+
timestamps
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
13
17
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe 'DeclareSchema Migration Generator' do
|
4
|
+
before do
|
5
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "generates nested models" do
|
9
|
+
Rails::Generators.invoke('declare_schema:model', %w[alpha/beta one:string two:integer])
|
10
|
+
|
11
|
+
expect(File.exist?('app/models/alpha/beta.rb')).to be_truthy
|
12
|
+
|
13
|
+
expect(File.read('app/models/alpha/beta.rb')).to eq(<<~EOS)
|
14
|
+
class Alpha::Beta < #{active_record_base_class}
|
15
|
+
|
16
|
+
fields do
|
17
|
+
one :string, limit: 255
|
18
|
+
two :integer
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
EOS
|
23
|
+
|
24
|
+
expect(File.read('app/models/alpha.rb')).to eq(<<~EOS)
|
25
|
+
module Alpha
|
26
|
+
def self.table_name_prefix
|
27
|
+
'alpha_'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
EOS
|
31
|
+
|
32
|
+
expect(File.read('test/models/alpha/beta_test.rb')).to eq(<<~EOS)
|
33
|
+
require 'test_helper'
|
34
|
+
|
35
|
+
class Alpha::BetaTest < ActiveSupport::TestCase
|
36
|
+
# test "the truth" do
|
37
|
+
# assert true
|
38
|
+
# end
|
39
|
+
end
|
40
|
+
EOS
|
41
|
+
|
42
|
+
expect(File.exist?('test/fixtures/alpha/beta.yml')).to be_truthy
|
43
|
+
|
44
|
+
$LOAD_PATH << "#{TESTAPP_PATH}/app/models"
|
45
|
+
|
46
|
+
expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
|
47
|
+
|
48
|
+
expect(File.exist?('db/schema.rb')).to be_truthy
|
49
|
+
|
50
|
+
expect(File.exist?("db/development.sqlite3") || File.exist?("db/test.sqlite3")).to be_truthy
|
51
|
+
|
52
|
+
module Alpha; end
|
53
|
+
require 'alpha/beta'
|
54
|
+
|
55
|
+
expect { Alpha::Beta }.to_not raise_exception
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe 'DeclareSchema Migration Generator interactive primary key' do
|
4
|
+
before do
|
5
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "allows alternate primary keys" do
|
9
|
+
class Foo < ActiveRecord::Base
|
10
|
+
fields do
|
11
|
+
end
|
12
|
+
self.primary_key = "foo_id"
|
13
|
+
end
|
14
|
+
|
15
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
16
|
+
expect(Foo.primary_key).to eq('foo_id')
|
17
|
+
|
18
|
+
### migrate from
|
19
|
+
# rename from custom primary_key
|
20
|
+
class Foo < ActiveRecord::Base
|
21
|
+
fields do
|
22
|
+
end
|
23
|
+
self.primary_key = "id"
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
|
27
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
28
|
+
expect(Foo.primary_key).to eq('id')
|
29
|
+
|
30
|
+
nuke_model_class(Foo)
|
31
|
+
|
32
|
+
### migrate to
|
33
|
+
|
34
|
+
# rename to custom primary_key
|
35
|
+
class Foo < ActiveRecord::Base
|
36
|
+
fields do
|
37
|
+
end
|
38
|
+
self.primary_key = "foo_id"
|
39
|
+
end
|
40
|
+
|
41
|
+
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
42
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
43
|
+
expect(Foo.primary_key).to eq('foo_id')
|
44
|
+
|
45
|
+
### ensure it doesn't cause further migrations
|
46
|
+
|
47
|
+
# check no further migrations
|
48
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
49
|
+
expect(up).to eq("")
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,795 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
|
5
|
+
RSpec.describe 'DeclareSchema Migration Generator' do
|
6
|
+
before do
|
7
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
8
|
+
end
|
9
|
+
|
10
|
+
# DeclareSchema - Migration Generator
|
11
|
+
it 'generates migrations' do
|
12
|
+
## The migration generator -- introduction
|
13
|
+
|
14
|
+
up_down = Generators::DeclareSchema::Migration::Migrator.run
|
15
|
+
expect(up_down).to eq(["", ""])
|
16
|
+
|
17
|
+
class Advert < ActiveRecord::Base
|
18
|
+
end
|
19
|
+
|
20
|
+
up_down = Generators::DeclareSchema::Migration::Migrator.run
|
21
|
+
expect(up_down).to eq(["", ""])
|
22
|
+
|
23
|
+
Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
|
24
|
+
|
25
|
+
Advert.connection.schema_cache.clear!
|
26
|
+
Advert.reset_column_information
|
27
|
+
|
28
|
+
class Advert < ActiveRecord::Base
|
29
|
+
fields do
|
30
|
+
name :string, limit: 255, null: true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
34
|
+
expect(up).to eq(<<~EOS.strip)
|
35
|
+
create_table :adverts, id: :bigint do |t|
|
36
|
+
t.string :name, limit: 255
|
37
|
+
end
|
38
|
+
EOS
|
39
|
+
expect(down).to eq("drop_table :adverts")
|
40
|
+
|
41
|
+
ActiveRecord::Migration.class_eval(up)
|
42
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "name"])
|
43
|
+
|
44
|
+
class Advert < ActiveRecord::Base
|
45
|
+
fields do
|
46
|
+
name :string, limit: 255, null: true
|
47
|
+
body :text, null: true
|
48
|
+
published_at :datetime, null: true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
up, down = migrate
|
52
|
+
expect(up).to eq(<<~EOS.strip)
|
53
|
+
add_column :adverts, :body, :text
|
54
|
+
add_column :adverts, :published_at, :datetime
|
55
|
+
|
56
|
+
add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
|
57
|
+
EOS
|
58
|
+
# TODO: ^ TECH-4975 add_index should not be there
|
59
|
+
|
60
|
+
expect(down).to eq(<<~EOS.strip)
|
61
|
+
remove_column :adverts, :body
|
62
|
+
remove_column :adverts, :published_at
|
63
|
+
EOS
|
64
|
+
|
65
|
+
Advert.field_specs.clear # not normally needed
|
66
|
+
class Advert < ActiveRecord::Base
|
67
|
+
fields do
|
68
|
+
name :string, limit: 255, null: true
|
69
|
+
body :text, null: true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
up, down = migrate
|
74
|
+
expect(up).to eq("remove_column :adverts, :published_at")
|
75
|
+
expect(down).to eq("add_column :adverts, :published_at, :datetime")
|
76
|
+
|
77
|
+
nuke_model_class(Advert)
|
78
|
+
class Advert < ActiveRecord::Base
|
79
|
+
fields do
|
80
|
+
title :string, limit: 255, null: true
|
81
|
+
body :text, null: true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
86
|
+
expect(up).to eq(<<~EOS.strip)
|
87
|
+
add_column :adverts, :title, :string, limit: 255
|
88
|
+
remove_column :adverts, :name
|
89
|
+
EOS
|
90
|
+
|
91
|
+
expect(down).to eq(<<~EOS.strip)
|
92
|
+
remove_column :adverts, :title
|
93
|
+
add_column :adverts, :name, :string, limit: 255
|
94
|
+
EOS
|
95
|
+
|
96
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })
|
97
|
+
expect(up).to eq("rename_column :adverts, :name, :title")
|
98
|
+
expect(down).to eq("rename_column :adverts, :title, :name")
|
99
|
+
|
100
|
+
migrate
|
101
|
+
|
102
|
+
class Advert < ActiveRecord::Base
|
103
|
+
fields do
|
104
|
+
title :text, null: true
|
105
|
+
body :text, null: true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
up_down = Generators::DeclareSchema::Migration::Migrator.run
|
110
|
+
expect(up_down).to eq(["change_column :adverts, :title, :text",
|
111
|
+
"change_column :adverts, :title, :string, limit: 255"])
|
112
|
+
|
113
|
+
class Advert < ActiveRecord::Base
|
114
|
+
fields do
|
115
|
+
title :string, default: "Untitled", limit: 255, null: true
|
116
|
+
body :text, null: true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
up, down = migrate
|
121
|
+
expect(up.split(',').slice(0,3).join(',')).to eq('change_column :adverts, :title, :string')
|
122
|
+
expect(up.split(',').slice(3,2).sort.join(',')).to eq(" default: \"Untitled\", limit: 255")
|
123
|
+
expect(down).to eq("change_column :adverts, :title, :string, limit: 255")
|
124
|
+
|
125
|
+
|
126
|
+
### Limits
|
127
|
+
|
128
|
+
class Advert < ActiveRecord::Base
|
129
|
+
fields do
|
130
|
+
price :integer, null: true, limit: 2
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
135
|
+
expect(up).to eq("add_column :adverts, :price, :integer, limit: 2")
|
136
|
+
|
137
|
+
# Now run the migration, then change the limit:
|
138
|
+
|
139
|
+
ActiveRecord::Migration.class_eval(up)
|
140
|
+
class Advert < ActiveRecord::Base
|
141
|
+
fields do
|
142
|
+
price :integer, null: true, limit: 3
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
147
|
+
expect(up).to eq("change_column :adverts, :price, :integer, limit: 3")
|
148
|
+
expect(down).to eq("change_column :adverts, :price, :integer, limit: 2")
|
149
|
+
|
150
|
+
# Note that limit on a decimal column is ignored (use :scale and :precision)
|
151
|
+
|
152
|
+
ActiveRecord::Migration.class_eval("remove_column :adverts, :price")
|
153
|
+
class Advert < ActiveRecord::Base
|
154
|
+
fields do
|
155
|
+
price :decimal, null: true, limit: 4
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
160
|
+
expect(up).to eq("add_column :adverts, :price, :decimal")
|
161
|
+
|
162
|
+
# Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
|
163
|
+
# allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
|
164
|
+
# If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
|
165
|
+
# that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
|
166
|
+
|
167
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_falsey
|
168
|
+
class Advert < ActiveRecord::Base
|
169
|
+
fields do
|
170
|
+
notes :text
|
171
|
+
description :text, limit: 30000
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
176
|
+
expect(up).to eq(<<~EOS.strip)
|
177
|
+
add_column :adverts, :price, :decimal
|
178
|
+
add_column :adverts, :notes, :text, null: false
|
179
|
+
add_column :adverts, :description, :text, null: false
|
180
|
+
EOS
|
181
|
+
|
182
|
+
# (There is no limit on `add_column ... :description` above since these tests are run against SQLite.)
|
183
|
+
|
184
|
+
Advert.field_specs.delete :price
|
185
|
+
Advert.field_specs.delete :notes
|
186
|
+
Advert.field_specs.delete :description
|
187
|
+
|
188
|
+
# In MySQL, limits are applied, rounded up:
|
189
|
+
|
190
|
+
::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
|
191
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
192
|
+
class Advert < ActiveRecord::Base
|
193
|
+
fields do
|
194
|
+
notes :text
|
195
|
+
description :text, limit: 200
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
200
|
+
expect(up).to eq(<<~EOS.strip)
|
201
|
+
add_column :adverts, :notes, :text, null: false, limit: 4294967295
|
202
|
+
add_column :adverts, :description, :text, null: false, limit: 255
|
203
|
+
EOS
|
204
|
+
|
205
|
+
Advert.field_specs.delete :notes
|
206
|
+
|
207
|
+
# Limits that are too high for MySQL will raise an exception.
|
208
|
+
|
209
|
+
::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
|
210
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
211
|
+
expect do
|
212
|
+
class Advert < ActiveRecord::Base
|
213
|
+
fields do
|
214
|
+
notes :text
|
215
|
+
description :text, limit: 0x1_0000_0000
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
|
219
|
+
|
220
|
+
Advert.field_specs.delete :notes
|
221
|
+
|
222
|
+
# And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
|
223
|
+
|
224
|
+
# To start, we'll set the database schema for `description` to match the above limit of 255.
|
225
|
+
|
226
|
+
expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
|
227
|
+
Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
|
228
|
+
Advert.connection.schema_cache.clear!
|
229
|
+
Advert.reset_column_information
|
230
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
231
|
+
to eq(["adverts"])
|
232
|
+
expect(Advert.columns.map(&:name)).to eq(["id", "body", "title", "description"])
|
233
|
+
|
234
|
+
# Now migrate to an unstated text limit:
|
235
|
+
|
236
|
+
class Advert < ActiveRecord::Base
|
237
|
+
fields do
|
238
|
+
description :text
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
243
|
+
expect(up).to eq("change_column :adverts, :description, :text, limit: 4294967295, null: false")
|
244
|
+
expect(down).to eq("change_column :adverts, :description, :text")
|
245
|
+
|
246
|
+
# TODO TECH-4814: The above test should have this output:
|
247
|
+
# TODO => "change_column :adverts, :description, :text, limit: 255
|
248
|
+
|
249
|
+
# And migrate to a stated text limit that is the same as the unstated one:
|
250
|
+
|
251
|
+
class Advert < ActiveRecord::Base
|
252
|
+
fields do
|
253
|
+
description :text, limit: 0xffffffff
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
258
|
+
expect(up).to eq("change_column :adverts, :description, :text, limit: 4294967295, null: false")
|
259
|
+
expect(down).to eq("change_column :adverts, :description, :text")
|
260
|
+
::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, false)
|
261
|
+
|
262
|
+
Advert.field_specs.clear
|
263
|
+
Advert.connection.schema_cache.clear!
|
264
|
+
Advert.reset_column_information
|
265
|
+
class Advert < ActiveRecord::Base
|
266
|
+
fields do
|
267
|
+
name :string, limit: 255, null: true
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
272
|
+
ActiveRecord::Migration.class_eval up
|
273
|
+
Advert.connection.schema_cache.clear!
|
274
|
+
Advert.reset_column_information
|
275
|
+
|
276
|
+
### Foreign Keys
|
277
|
+
|
278
|
+
# DeclareSchema extends the `belongs_to` macro so that it also declares the
|
279
|
+
# foreign-key field. It also generates an index on the field.
|
280
|
+
|
281
|
+
class Category < ActiveRecord::Base; end
|
282
|
+
class Advert < ActiveRecord::Base
|
283
|
+
fields do
|
284
|
+
name :string, limit: 255, null: true
|
285
|
+
end
|
286
|
+
belongs_to :category
|
287
|
+
end
|
288
|
+
|
289
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
290
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
291
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
292
|
+
add_index :adverts, [:category_id], name: 'on_category_id'
|
293
|
+
EOS
|
294
|
+
expect(down.sub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
295
|
+
remove_column :adverts, :category_id
|
296
|
+
remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid
|
297
|
+
EOS
|
298
|
+
|
299
|
+
Advert.field_specs.delete(:category_id)
|
300
|
+
Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
301
|
+
|
302
|
+
# If you specify a custom foreign key, the migration generator observes that:
|
303
|
+
|
304
|
+
class Category < ActiveRecord::Base; end
|
305
|
+
class Advert < ActiveRecord::Base
|
306
|
+
fields { }
|
307
|
+
belongs_to :category, foreign_key: "c_id", class_name: 'Category'
|
308
|
+
end
|
309
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
310
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
311
|
+
add_column :adverts, :c_id, :integer, limit: 8, null: false
|
312
|
+
add_index :adverts, [:c_id], name: 'on_c_id'
|
313
|
+
EOS
|
314
|
+
|
315
|
+
Advert.field_specs.delete(:c_id)
|
316
|
+
Advert.index_specs.delete_if { |spec| spec.fields == ["c_id"] }
|
317
|
+
|
318
|
+
# You can avoid generating the index by specifying `index: false`
|
319
|
+
|
320
|
+
class Category < ActiveRecord::Base; end
|
321
|
+
class Advert < ActiveRecord::Base
|
322
|
+
fields { }
|
323
|
+
belongs_to :category, index: false
|
324
|
+
end
|
325
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
326
|
+
expect(up.gsub(/\n+/, "\n")).to eq("add_column :adverts, :category_id, :integer, limit: 8, null: false")
|
327
|
+
|
328
|
+
Advert.field_specs.delete(:category_id)
|
329
|
+
Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
|
330
|
+
|
331
|
+
# You can specify the index name with :index
|
332
|
+
|
333
|
+
class Category < ActiveRecord::Base; end
|
334
|
+
class Advert < ActiveRecord::Base
|
335
|
+
fields { }
|
336
|
+
belongs_to :category, index: 'my_index'
|
337
|
+
end
|
338
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
339
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
340
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
341
|
+
add_index :adverts, [:category_id], name: 'my_index'
|
342
|
+
EOS
|
343
|
+
|
344
|
+
Advert.field_specs.delete(:category_id)
|
345
|
+
Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
|
346
|
+
|
347
|
+
### Timestamps and Optimimistic Locking
|
348
|
+
|
349
|
+
# `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
|
350
|
+
# Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
|
351
|
+
|
352
|
+
class Advert < ActiveRecord::Base
|
353
|
+
fields do
|
354
|
+
timestamps
|
355
|
+
optimistic_lock
|
356
|
+
end
|
357
|
+
end
|
358
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
359
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
360
|
+
add_column :adverts, :created_at, :datetime
|
361
|
+
add_column :adverts, :updated_at, :datetime
|
362
|
+
add_column :adverts, :lock_version, :integer, null: false, default: 1
|
363
|
+
EOS
|
364
|
+
expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
365
|
+
remove_column :adverts, :created_at
|
366
|
+
remove_column :adverts, :updated_at
|
367
|
+
remove_column :adverts, :lock_version
|
368
|
+
EOS
|
369
|
+
|
370
|
+
Advert.field_specs.delete(:updated_at)
|
371
|
+
Advert.field_specs.delete(:created_at)
|
372
|
+
Advert.field_specs.delete(:lock_version)
|
373
|
+
|
374
|
+
### Indices
|
375
|
+
|
376
|
+
# You can add an index to a field definition
|
377
|
+
|
378
|
+
class Advert < ActiveRecord::Base
|
379
|
+
fields do
|
380
|
+
title :string, index: true, limit: 255, null: true
|
381
|
+
end
|
382
|
+
end
|
383
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
384
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
385
|
+
add_column :adverts, :title, :string, limit: 255
|
386
|
+
add_index :adverts, [:title], name: 'on_title'
|
387
|
+
EOS
|
388
|
+
|
389
|
+
Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
|
390
|
+
|
391
|
+
# You can ask for a unique index
|
392
|
+
|
393
|
+
class Advert < ActiveRecord::Base
|
394
|
+
fields do
|
395
|
+
title :string, index: true, unique: true, null: true, limit: 255
|
396
|
+
end
|
397
|
+
end
|
398
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
399
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
400
|
+
add_column :adverts, :title, :string, limit: 255
|
401
|
+
add_index :adverts, [:title], unique: true, name: 'on_title'
|
402
|
+
EOS
|
403
|
+
|
404
|
+
Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
|
405
|
+
|
406
|
+
# You can specify the name for the index
|
407
|
+
|
408
|
+
class Advert < ActiveRecord::Base
|
409
|
+
fields do
|
410
|
+
title :string, index: 'my_index', limit: 255, null: true
|
411
|
+
end
|
412
|
+
end
|
413
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
414
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
415
|
+
add_column :adverts, :title, :string, limit: 255
|
416
|
+
add_index :adverts, [:title], name: 'my_index'
|
417
|
+
EOS
|
418
|
+
|
419
|
+
Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
|
420
|
+
|
421
|
+
# You can ask for an index outside of the fields block
|
422
|
+
|
423
|
+
class Advert < ActiveRecord::Base
|
424
|
+
index :title
|
425
|
+
end
|
426
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
427
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
428
|
+
add_column :adverts, :title, :string, limit: 255
|
429
|
+
add_index :adverts, [:title], name: 'on_title'
|
430
|
+
EOS
|
431
|
+
|
432
|
+
Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
|
433
|
+
|
434
|
+
# The available options for the index function are `:unique` and `:name`
|
435
|
+
|
436
|
+
class Advert < ActiveRecord::Base
|
437
|
+
index :title, unique: true, name: 'my_index'
|
438
|
+
end
|
439
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
440
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
441
|
+
add_column :adverts, :title, :string, limit: 255
|
442
|
+
add_index :adverts, [:title], unique: true, name: 'my_index'
|
443
|
+
EOS
|
444
|
+
|
445
|
+
Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
|
446
|
+
|
447
|
+
# You can create an index on more than one field
|
448
|
+
|
449
|
+
class Advert < ActiveRecord::Base
|
450
|
+
index [:title, :category_id]
|
451
|
+
end
|
452
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
453
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
454
|
+
add_column :adverts, :title, :string, limit: 255
|
455
|
+
add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'
|
456
|
+
EOS
|
457
|
+
|
458
|
+
Advert.index_specs.delete_if { |spec| spec.fields==["title", "category_id"] }
|
459
|
+
|
460
|
+
# Finally, you can specify that the migration generator should completely ignore an
|
461
|
+
# index by passing its name to ignore_index in the model.
|
462
|
+
# This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
|
463
|
+
|
464
|
+
### Rename a table
|
465
|
+
|
466
|
+
# 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.
|
467
|
+
|
468
|
+
class Advert < ActiveRecord::Base
|
469
|
+
self.table_name = "ads"
|
470
|
+
fields do
|
471
|
+
title :string, limit: 255, null: true
|
472
|
+
body :text, null: true
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
Advert.connection.schema_cache.clear!
|
477
|
+
Advert.reset_column_information
|
478
|
+
|
479
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")
|
480
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
481
|
+
rename_table :adverts, :ads
|
482
|
+
add_column :ads, :title, :string, limit: 255
|
483
|
+
add_column :ads, :body, :text
|
484
|
+
add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
|
485
|
+
EOS
|
486
|
+
expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
487
|
+
remove_column :ads, :title
|
488
|
+
remove_column :ads, :body
|
489
|
+
rename_table :ads, :adverts
|
490
|
+
add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
|
491
|
+
EOS
|
492
|
+
|
493
|
+
# Set the table name back to what it should be and confirm we're in sync:
|
494
|
+
|
495
|
+
Advert.field_specs.delete(:title)
|
496
|
+
Advert.field_specs.delete(:body)
|
497
|
+
class Advert < ActiveRecord::Base
|
498
|
+
self.table_name = "adverts"
|
499
|
+
end
|
500
|
+
|
501
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
|
502
|
+
|
503
|
+
### Rename a table
|
504
|
+
|
505
|
+
# 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.
|
506
|
+
|
507
|
+
nuke_model_class(Advert)
|
508
|
+
|
509
|
+
class Advertisement < ActiveRecord::Base
|
510
|
+
fields do
|
511
|
+
title :string, limit: 255, null: true
|
512
|
+
body :text, null: true
|
513
|
+
end
|
514
|
+
end
|
515
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")
|
516
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
517
|
+
rename_table :adverts, :advertisements
|
518
|
+
add_column :advertisements, :title, :string, limit: 255
|
519
|
+
add_column :advertisements, :body, :text
|
520
|
+
remove_column :advertisements, :name
|
521
|
+
add_index :advertisements, [:id], unique: true, name: 'PRIMARY_KEY'
|
522
|
+
EOS
|
523
|
+
expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
524
|
+
remove_column :advertisements, :title
|
525
|
+
remove_column :advertisements, :body
|
526
|
+
add_column :adverts, :name, :string, limit: 255
|
527
|
+
rename_table :advertisements, :adverts
|
528
|
+
add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
|
529
|
+
EOS
|
530
|
+
|
531
|
+
### Drop a table
|
532
|
+
|
533
|
+
nuke_model_class(Advertisement)
|
534
|
+
|
535
|
+
# If you delete a model, the migration generator will create a `drop_table` migration.
|
536
|
+
|
537
|
+
# Dropping tables is where the automatic down-migration really comes in handy:
|
538
|
+
|
539
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
540
|
+
expect(up).to eq("drop_table :adverts")
|
541
|
+
expect(down.gsub(/,.*/m, '')).to eq("create_table \"adverts\"")
|
542
|
+
|
543
|
+
## STI
|
544
|
+
|
545
|
+
### Adding an STI subclass
|
546
|
+
|
547
|
+
# Adding a subclass or two should introduce the 'type' column and no other changes
|
548
|
+
|
549
|
+
class Advert < ActiveRecord::Base
|
550
|
+
fields do
|
551
|
+
body :text, null: true
|
552
|
+
title :string, default: "Untitled", limit: 255, null: true
|
553
|
+
end
|
554
|
+
end
|
555
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
556
|
+
ActiveRecord::Migration.class_eval(up)
|
557
|
+
|
558
|
+
class FancyAdvert < Advert
|
559
|
+
end
|
560
|
+
class SuperFancyAdvert < FancyAdvert
|
561
|
+
end
|
562
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run
|
563
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
564
|
+
add_column :adverts, :type, :string, limit: 255
|
565
|
+
add_index :adverts, [:type], name: 'on_type'
|
566
|
+
EOS
|
567
|
+
expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
568
|
+
remove_column :adverts, :type
|
569
|
+
remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid
|
570
|
+
EOS
|
571
|
+
|
572
|
+
Advert.field_specs.delete(:type)
|
573
|
+
nuke_model_class(SuperFancyAdvert)
|
574
|
+
nuke_model_class(FancyAdvert)
|
575
|
+
Advert.index_specs.delete_if { |spec| spec.fields==["type"] }
|
576
|
+
|
577
|
+
## Coping with multiple changes
|
578
|
+
|
579
|
+
# The migration generator is designed to create complete migrations even if many changes to the models have taken place.
|
580
|
+
|
581
|
+
# First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
|
582
|
+
|
583
|
+
ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
|
584
|
+
Advert.connection.schema_cache.clear!
|
585
|
+
Advert.reset_column_information
|
586
|
+
|
587
|
+
expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
|
588
|
+
to eq(["adverts"])
|
589
|
+
expect(Advert.columns.map(&:name).sort).to eq(["body", "id", "title"])
|
590
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
|
591
|
+
|
592
|
+
|
593
|
+
### Rename a column and change the default
|
594
|
+
|
595
|
+
Advert.field_specs.clear
|
596
|
+
|
597
|
+
class Advert < ActiveRecord::Base
|
598
|
+
fields do
|
599
|
+
name :string, default: "No Name", limit: 255, null: true
|
600
|
+
body :text, null: true
|
601
|
+
end
|
602
|
+
end
|
603
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })
|
604
|
+
expect(up).to eq(<<~EOS.strip)
|
605
|
+
rename_column :adverts, :title, :name
|
606
|
+
change_column :adverts, :name, :string, limit: 255, default: \"No Name\"
|
607
|
+
EOS
|
608
|
+
|
609
|
+
expect(down).to eq(<<~EOS.strip)
|
610
|
+
rename_column :adverts, :name, :title
|
611
|
+
change_column :adverts, :title, :string, limit: 255, default: \"Untitled\"
|
612
|
+
EOS
|
613
|
+
|
614
|
+
### Rename a table and add a column
|
615
|
+
|
616
|
+
nuke_model_class(Advert)
|
617
|
+
class Ad < ActiveRecord::Base
|
618
|
+
fields do
|
619
|
+
title :string, default: "Untitled", limit: 255
|
620
|
+
body :text, null: true
|
621
|
+
created_at :datetime
|
622
|
+
end
|
623
|
+
end
|
624
|
+
up = Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads).first
|
625
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
626
|
+
rename_table :adverts, :ads
|
627
|
+
add_column :ads, :created_at, :datetime, null: false
|
628
|
+
change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
|
629
|
+
add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
|
630
|
+
EOS
|
631
|
+
|
632
|
+
class Advert < ActiveRecord::Base
|
633
|
+
fields do
|
634
|
+
body :text, null: true
|
635
|
+
title :string, default: "Untitled", limit: 255, null: true
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
## Legacy Keys
|
640
|
+
|
641
|
+
# DeclareSchema has some support for legacy keys.
|
642
|
+
|
643
|
+
nuke_model_class(Ad)
|
644
|
+
|
645
|
+
class Advert < ActiveRecord::Base
|
646
|
+
fields do
|
647
|
+
body :text, null: true
|
648
|
+
end
|
649
|
+
self.primary_key = "advert_id"
|
650
|
+
end
|
651
|
+
up, _down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })
|
652
|
+
expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
|
653
|
+
rename_column :adverts, :id, :advert_id
|
654
|
+
add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY_KEY'
|
655
|
+
EOS
|
656
|
+
|
657
|
+
nuke_model_class(Advert)
|
658
|
+
ActiveRecord::Base.connection.execute("drop table `adverts`;")
|
659
|
+
|
660
|
+
## DSL
|
661
|
+
|
662
|
+
# The DSL allows lambdas and constants
|
663
|
+
|
664
|
+
class User < ActiveRecord::Base
|
665
|
+
fields do
|
666
|
+
company :string, limit: 255, ruby_default: -> { "BigCorp" }
|
667
|
+
end
|
668
|
+
end
|
669
|
+
expect(User.field_specs.keys).to eq(['company'])
|
670
|
+
expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
|
671
|
+
|
672
|
+
## validates
|
673
|
+
|
674
|
+
# DeclareSchema can accept a validates hash in the field options.
|
675
|
+
|
676
|
+
class Ad < ActiveRecord::Base
|
677
|
+
class << self
|
678
|
+
def validates(field_name, options)
|
679
|
+
end
|
680
|
+
end
|
681
|
+
end
|
682
|
+
expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
|
683
|
+
class Ad < ActiveRecord::Base
|
684
|
+
fields do
|
685
|
+
company :string, limit: 255, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
|
686
|
+
end
|
687
|
+
self.primary_key = "advert_id"
|
688
|
+
end
|
689
|
+
up, _down = Generators::DeclareSchema::Migration::Migrator.run
|
690
|
+
ActiveRecord::Migration.class_eval(up)
|
691
|
+
expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
|
692
|
+
end
|
693
|
+
|
694
|
+
if Rails::VERSION::MAJOR >= 5
|
695
|
+
describe 'belongs_to' do
|
696
|
+
before do
|
697
|
+
unless defined?(AdCategory)
|
698
|
+
class AdCategory < ActiveRecord::Base
|
699
|
+
fields { }
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
class Advert < ActiveRecord::Base
|
704
|
+
fields do
|
705
|
+
name :string, limit: 255, null: true
|
706
|
+
category_id :integer, limit: 8
|
707
|
+
nullable_category_id :integer, limit: 8, null: true
|
708
|
+
end
|
709
|
+
end
|
710
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
711
|
+
ActiveRecord::Migration.class_eval(up)
|
712
|
+
end
|
713
|
+
|
714
|
+
it 'passes through optional: when given' do
|
715
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
716
|
+
self.table_name = 'adverts'
|
717
|
+
fields { }
|
718
|
+
reset_column_information
|
719
|
+
belongs_to :ad_category, optional: true
|
720
|
+
end
|
721
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: true)
|
722
|
+
end
|
723
|
+
|
724
|
+
describe 'contradictory settings' do # contradictory settings are ok during migration
|
725
|
+
it 'passes through optional: true, null: false' do
|
726
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
727
|
+
self.table_name = 'adverts'
|
728
|
+
fields { }
|
729
|
+
reset_column_information
|
730
|
+
belongs_to :ad_category, optional: true, null: false
|
731
|
+
end
|
732
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: true)
|
733
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(false)
|
734
|
+
end
|
735
|
+
|
736
|
+
it 'passes through optional: false, null: true' do
|
737
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
738
|
+
self.table_name = 'adverts'
|
739
|
+
fields { }
|
740
|
+
reset_column_information
|
741
|
+
belongs_to :ad_category, optional: false, null: true
|
742
|
+
end
|
743
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: false)
|
744
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(true)
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
[false, true].each do |nullable|
|
749
|
+
context "nullable=#{nullable}" do
|
750
|
+
it 'infers optional: from null:' do
|
751
|
+
eval <<~EOS
|
752
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
753
|
+
fields { }
|
754
|
+
belongs_to :ad_category, null: #{nullable}
|
755
|
+
end
|
756
|
+
EOS
|
757
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: nullable)
|
758
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
759
|
+
end
|
760
|
+
|
761
|
+
it 'infers null: from optional:' do
|
762
|
+
eval <<~EOS
|
763
|
+
class AdvertBelongsTo < ActiveRecord::Base
|
764
|
+
fields { }
|
765
|
+
belongs_to :ad_category, optional: #{nullable}
|
766
|
+
end
|
767
|
+
EOS
|
768
|
+
expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: nullable)
|
769
|
+
expect(AdvertBelongsTo.field_specs['ad_category_id'].options&.[](:null)).to eq(nullable)
|
770
|
+
end
|
771
|
+
end
|
772
|
+
end
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
describe 'migration base class' do
|
777
|
+
it 'adapts to Rails 4' do
|
778
|
+
class Advert < active_record_base_class.constantize
|
779
|
+
fields do
|
780
|
+
title :string, limit: 100
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
785
|
+
|
786
|
+
migrations = Dir.glob('db/migrate/*declare_schema_migration*.rb')
|
787
|
+
expect(migrations.size).to eq(1), migrations.inspect
|
788
|
+
|
789
|
+
migration_content = File.read(migrations.first)
|
790
|
+
first_line = migration_content.split("\n").first
|
791
|
+
base_class = first_line.split(' < ').last
|
792
|
+
expect(base_class).to eq("(Rails::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration)")
|
793
|
+
end
|
794
|
+
end
|
795
|
+
end
|