declare_schema 0.1.3 → 0.2.0.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +1 -3
- 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 +0 -1
- data/lib/declare_schema/model/field_spec.rb +0 -11
- 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 +13 -6
- 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 +686 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +29 -0
- data/spec/spec_helper.rb +26 -0
- metadata +10 -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
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
TESTAPP_PATH = ENV['TESTAPP_PATH'] || File.join(Dir.tmpdir, 'declare_schema_testapp') unless defined?(TESTAPP_PATH)
|
7
|
+
FileUtils.chdir(TESTAPP_PATH)
|
8
|
+
|
9
|
+
system "rm -rf app/models/ad* app/models/alpha*"
|
10
|
+
system "rm -rf test/models/ad* test/models/alpha*"
|
11
|
+
system "rm -rf test/fixtures/ad* test/fixtures/alpha*"
|
12
|
+
system "rm -rf db/migrate/*"
|
13
|
+
system "mkdir -p #{TESTAPP_PATH}/app/assets/config"
|
14
|
+
system "echo '' >> #{TESTAPP_PATH}/app/assets/config/manifest.js"
|
15
|
+
|
16
|
+
require "#{TESTAPP_PATH}/config/environment"
|
17
|
+
|
18
|
+
require 'rails/generators'
|
19
|
+
Rails::Generators.configure!(Rails.application.config.generators)
|
20
|
+
|
21
|
+
(ActiveRecord::Base.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).each do |table|
|
22
|
+
ActiveRecord::Base.connection.execute("DROP TABLE #{ActiveRecord::Base.connection.quote_table_name(table)}")
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveRecord::Base.send(:descendants).each do |model|
|
26
|
+
unless model.name['Active'] || model.name['Application']
|
27
|
+
nuke_model_class(model)
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -22,6 +22,32 @@ RSpec.configure do |config|
|
|
22
22
|
|
23
23
|
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 2_000
|
24
24
|
|
25
|
+
def active_record_base_class
|
26
|
+
if Rails::VERSION::MAJOR == 4
|
27
|
+
'ActiveRecord::Base'
|
28
|
+
else
|
29
|
+
'ApplicationRecord'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def migrate(renames = {})
|
34
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run(renames)
|
35
|
+
ActiveRecord::Migration.class_eval(up)
|
36
|
+
ActiveRecord::Base.send(:descendants).each { |model| model.reset_column_information }
|
37
|
+
[up, down]
|
38
|
+
end
|
39
|
+
|
40
|
+
def nuke_model_class(klass)
|
41
|
+
ActiveSupport::DescendantsTracker.instance_eval do
|
42
|
+
direct_descendants = class_variable_get('@@direct_descendants')
|
43
|
+
direct_descendants[ActiveRecord::Base] = direct_descendants[ActiveRecord::Base].to_a.reject { |descendant| descendant == klass }
|
44
|
+
if defined?(ApplicationRecord)
|
45
|
+
direct_descendants[ApplicationRecord] = direct_descendants[ApplicationRecord].to_a.reject { |descendant| descendant == klass }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
Object.instance_eval { remove_const(klass.name.to_sym) rescue nil }
|
49
|
+
end
|
50
|
+
|
25
51
|
def with_modified_env(options, &block)
|
26
52
|
ClimateControl.modify(options, &block)
|
27
53
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: declare_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.0.pre.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Invoca Development adapted from hobo_fields by Tom Locke
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-10-
|
11
|
+
date: 2020-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -68,24 +68,22 @@ files:
|
|
68
68
|
- lib/generators/declare_schema/migration/templates/migration.rb.erb
|
69
69
|
- lib/generators/declare_schema/model/USAGE
|
70
70
|
- lib/generators/declare_schema/model/model_generator.rb
|
71
|
-
- lib/generators/declare_schema/model/templates/model_injection.rb.erb
|
72
71
|
- lib/generators/declare_schema/support/eval_template.rb
|
73
72
|
- lib/generators/declare_schema/support/model.rb
|
74
73
|
- lib/generators/declare_schema/support/thor_shell.rb
|
74
|
+
- spec/lib/declare_schema/api_spec.rb
|
75
75
|
- spec/lib/declare_schema/field_declaration_dsl_spec.rb
|
76
|
+
- spec/lib/declare_schema/generator_spec.rb
|
77
|
+
- spec/lib/declare_schema/interactive_primary_key_spec.rb
|
78
|
+
- spec/lib/declare_schema/migration_generator_spec.rb
|
79
|
+
- spec/lib/declare_schema/prepare_testapp.rb
|
76
80
|
- spec/spec_helper.rb
|
77
|
-
- test/api.rdoctest
|
78
|
-
- test/generators.rdoctest
|
79
|
-
- test/interactive_primary_key.rdoctest
|
80
|
-
- test/migration_generator.rdoctest
|
81
|
-
- test/migration_generator_comments.rdoctestDISABLED
|
82
|
-
- test/prepare_testapp.rb
|
83
81
|
- test_responses.txt
|
84
82
|
homepage: https://github.com/Invoca/declare_schema
|
85
83
|
licenses: []
|
86
84
|
metadata:
|
87
85
|
allowed_push_host: https://rubygems.org
|
88
|
-
post_install_message:
|
86
|
+
post_install_message:
|
89
87
|
rdoc_options: []
|
90
88
|
require_paths:
|
91
89
|
- lib
|
@@ -101,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
99
|
version: 1.3.6
|
102
100
|
requirements: []
|
103
101
|
rubygems_version: 3.0.3
|
104
|
-
signing_key:
|
102
|
+
signing_key:
|
105
103
|
specification_version: 4
|
106
104
|
summary: Database migration generator for Rails
|
107
105
|
test_files: []
|
@@ -1,25 +0,0 @@
|
|
1
|
-
|
2
|
-
fields do
|
3
|
-
<% for attribute in field_attributes -%>
|
4
|
-
<%= "%-#{max_attribute_length}s" % attribute.name %> :<%= attribute.type %><%=
|
5
|
-
case attribute.type.to_s
|
6
|
-
when 'string'
|
7
|
-
', limit: 255'
|
8
|
-
else
|
9
|
-
''
|
10
|
-
end
|
11
|
-
%>
|
12
|
-
<% end -%>
|
13
|
-
<% if options[:timestamps] -%>
|
14
|
-
timestamps
|
15
|
-
<% end -%>
|
16
|
-
end
|
17
|
-
|
18
|
-
<% for bt in bts -%>
|
19
|
-
belongs_to :<%= bt %>
|
20
|
-
<% end -%>
|
21
|
-
<%= "\n" unless bts.empty? -%>
|
22
|
-
<% for hm in hms -%>
|
23
|
-
has_many :<%= hm %>, dependent: :destroy
|
24
|
-
<% end -%>
|
25
|
-
<%= "\n" unless hms.empty? -%>
|
data/test/api.rdoctest
DELETED
@@ -1,136 +0,0 @@
|
|
1
|
-
# DeclareSchema API
|
2
|
-
|
3
|
-
In order for the API examples to run we need to load the rails generators of our testapp:
|
4
|
-
{.hidden}
|
5
|
-
|
6
|
-
doctest: prepare testapp environment
|
7
|
-
doctest_require: 'prepare_testapp'
|
8
|
-
{.hidden}
|
9
|
-
|
10
|
-
## Example Models
|
11
|
-
|
12
|
-
Let's define some example models that we can use to demonstrate the API. With DeclareSchema we can use the 'declare_schema:model' generator like so:
|
13
|
-
|
14
|
-
$ rails generate declare_schema:model advert title:string body:text
|
15
|
-
|
16
|
-
This will generate the test, fixture and a model file like this:
|
17
|
-
|
18
|
-
>> Rails::Generators.invoke 'declare_schema:model', %w(advert title:string body:text)
|
19
|
-
{.hidden}
|
20
|
-
|
21
|
-
class Advert < ActiveRecord::Base
|
22
|
-
fields do
|
23
|
-
title :string
|
24
|
-
body :text, limit: 0xffff, null: true
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
The migration generator uses this information to create a migration. The following creates and runs the migration so we're ready to go.
|
29
|
-
|
30
|
-
$ rails generate declare_schema:migration -n -m
|
31
|
-
|
32
|
-
We're now ready to start demonstrating the API
|
33
|
-
|
34
|
-
>> require_relative "#{Rails.root}/app/models/advert.rb" if Rails::VERSION::MAJOR > 5
|
35
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
36
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
37
|
-
{.hidden}
|
38
|
-
|
39
|
-
## The Basics
|
40
|
-
|
41
|
-
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.
|
42
|
-
|
43
|
-
### Field Types
|
44
|
-
|
45
|
-
Field values are returned as the type you specify.
|
46
|
-
|
47
|
-
>> a = Advert.new :body => "This is the body", id: 1, title: "title"
|
48
|
-
>> a.body.class
|
49
|
-
=> String
|
50
|
-
|
51
|
-
This also works after a round-trip to the database
|
52
|
-
|
53
|
-
>> a.save
|
54
|
-
>> b = Advert.find(a.id)
|
55
|
-
>> b.body.class
|
56
|
-
=> String
|
57
|
-
|
58
|
-
## Names vs. Classes
|
59
|
-
|
60
|
-
The full set of available symbolic names is
|
61
|
-
|
62
|
-
* `:integer`
|
63
|
-
* `:float`
|
64
|
-
* `:decimal`
|
65
|
-
* `:string`
|
66
|
-
* `:text`
|
67
|
-
* `:boolean`
|
68
|
-
* `:date`
|
69
|
-
* `:datetime`
|
70
|
-
* `:html`
|
71
|
-
* `:textile`
|
72
|
-
* `:markdown`
|
73
|
-
* `:password`
|
74
|
-
|
75
|
-
You can add your own types too. More on that later.
|
76
|
-
|
77
|
-
|
78
|
-
## Model extensions
|
79
|
-
|
80
|
-
DeclareSchema adds a few features to your models.
|
81
|
-
|
82
|
-
### `Model.attr_type`
|
83
|
-
|
84
|
-
Returns the type (i.e. class) declared for a given field or attribute
|
85
|
-
|
86
|
-
>> Advert.connection.schema_cache.clear!
|
87
|
-
>> Advert.reset_column_information
|
88
|
-
>> Advert.attr_type :title
|
89
|
-
=> String
|
90
|
-
>> Advert.attr_type :body
|
91
|
-
=> String
|
92
|
-
|
93
|
-
## Field validations
|
94
|
-
|
95
|
-
DeclareSchema gives you some shorthands for declaring some common validations right in the field declaration
|
96
|
-
|
97
|
-
### Required fields
|
98
|
-
|
99
|
-
The `:required` argument to a field gives a `validates_presence_of`:
|
100
|
-
|
101
|
-
>>
|
102
|
-
class Advert
|
103
|
-
fields do
|
104
|
-
title :string, :required, limit: 255
|
105
|
-
end
|
106
|
-
end
|
107
|
-
>> a = Advert.new
|
108
|
-
>> a.valid?
|
109
|
-
=> false
|
110
|
-
>> a.errors.full_messages
|
111
|
-
=> ["Title can't be blank"]
|
112
|
-
>> a.id = 2
|
113
|
-
>> a.body = "hello"
|
114
|
-
>> a.title = "Jimbo"
|
115
|
-
>> a.save
|
116
|
-
=> true
|
117
|
-
|
118
|
-
|
119
|
-
### Unique fields
|
120
|
-
|
121
|
-
The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
|
122
|
-
|
123
|
-
>>
|
124
|
-
class Advert
|
125
|
-
fields do
|
126
|
-
title :string, :unique, limit: 255
|
127
|
-
end
|
128
|
-
end
|
129
|
-
>> a = Advert.new :title => "Jimbo", id: 3, body: "hello"
|
130
|
-
>> a.valid?
|
131
|
-
=> false
|
132
|
-
>> a.errors.full_messages
|
133
|
-
=> ["Title has already been taken"]
|
134
|
-
>> a.title = "Sambo"
|
135
|
-
>> a.save
|
136
|
-
=> true
|
data/test/generators.rdoctest
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
doctest: prepare testapp environment
|
2
|
-
doctest_require: 'prepare_testapp'
|
3
|
-
|
4
|
-
doctest: generate declare_schema:model
|
5
|
-
>> begin; Rails::Generators.invoke 'declare_schema:model', %w(alpha/beta one:string two:integer); rescue => ex; $stderr.puts "#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}"; end
|
6
|
-
|
7
|
-
|
8
|
-
doctest: model file exists
|
9
|
-
>> File.exist? 'app/models/alpha/beta.rb'
|
10
|
-
=> true
|
11
|
-
|
12
|
-
doctest: model content matches
|
13
|
-
>> File.read 'app/models/alpha/beta.rb'
|
14
|
-
=> "class Alpha::Beta < #{Rails::VERSION::MAJOR > 4 ? 'ApplicationRecord' : 'ActiveRecord::Base'}\n\n fields do\n one :string, limit: 255\n two :integer\n end\n\nend\n"
|
15
|
-
|
16
|
-
doctest: module file exists
|
17
|
-
>> File.exist? 'app/models/alpha.rb'
|
18
|
-
=> true
|
19
|
-
|
20
|
-
doctest: module content matches
|
21
|
-
>> File.read 'app/models/alpha.rb'
|
22
|
-
=> "module Alpha\n def self.table_name_prefix\n 'alpha_'\n end\nend\n"
|
23
|
-
|
24
|
-
|
25
|
-
doctest: test file exists
|
26
|
-
>> File.exist? 'test/models/alpha/beta_test.rb'
|
27
|
-
=> true
|
28
|
-
|
29
|
-
doctest: test content matches
|
30
|
-
>> File.read 'test/models/alpha/beta_test.rb'
|
31
|
-
=>
|
32
|
-
require 'test_helper'
|
33
|
-
|
34
|
-
class Alpha::BetaTest < ActiveSupport::TestCase
|
35
|
-
# test "the truth" do
|
36
|
-
# assert true
|
37
|
-
# end
|
38
|
-
end
|
39
|
-
|
40
|
-
doctest: fixture file exists
|
41
|
-
>> File.exist? 'test/fixtures/alpha/beta.yml'
|
42
|
-
=> true
|
43
|
-
|
44
|
-
|
45
|
-
doctest: generate declare_schema:migration
|
46
|
-
>> puts "#{Rails.root}/app/models/alpha.rb"
|
47
|
-
>> require "#{Rails.root}/app/models/alpha.rb" if Rails::VERSION::MAJOR > 4
|
48
|
-
>> require "#{Rails.root}/app/models/alpha/beta.rb" if Rails::VERSION::MAJOR > 4
|
49
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
50
|
-
|
51
|
-
doctest: schema.rb file exists
|
52
|
-
>> system("ls -al db")
|
53
|
-
>> File.exist? 'db/schema.rb'
|
54
|
-
=> true
|
55
|
-
|
56
|
-
doctest: db file exists
|
57
|
-
>> File.exist?("db/development.sqlite3") || File.exist?("db/test.sqlite3")
|
58
|
-
=> true
|
59
|
-
|
60
|
-
doctest: Alpha::Beta class exists
|
61
|
-
>> Alpha::Beta
|
62
|
-
# will error if class doesn't exist
|
@@ -1,56 +0,0 @@
|
|
1
|
-
-*- indent-tabs-mode:nil; -*-
|
2
|
-
|
3
|
-
# DeclareSchema - Migration Generator
|
4
|
-
|
5
|
-
Our test requires to prepare the testapp:
|
6
|
-
{.hidden}
|
7
|
-
|
8
|
-
doctest_require: 'prepare_testapp'
|
9
|
-
|
10
|
-
{.hidden}
|
11
|
-
|
12
|
-
And requires also that you enter the right choice when prompted. OK we're ready to get going.
|
13
|
-
|
14
|
-
## Alternate Primary Keys
|
15
|
-
|
16
|
-
### create
|
17
|
-
doctest: create table with custom primary_key
|
18
|
-
>>
|
19
|
-
class Foo < ActiveRecord::Base
|
20
|
-
fields do
|
21
|
-
end
|
22
|
-
self.primary_key="foo_id"
|
23
|
-
end
|
24
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
25
|
-
>> Foo.primary_key
|
26
|
-
=> 'foo_id'
|
27
|
-
|
28
|
-
### migrate from
|
29
|
-
doctest: rename from custom primary_key
|
30
|
-
>>
|
31
|
-
class Foo < ActiveRecord::Base
|
32
|
-
self.primary_key="id"
|
33
|
-
end
|
34
|
-
puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
|
35
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
36
|
-
>> Foo.primary_key
|
37
|
-
=> 'id'
|
38
|
-
|
39
|
-
### migrate to
|
40
|
-
|
41
|
-
doctest: rename to custom primary_key
|
42
|
-
>>
|
43
|
-
class Foo < ActiveRecord::Base
|
44
|
-
self.primary_key="foo_id"
|
45
|
-
end
|
46
|
-
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
47
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
48
|
-
>> Foo.primary_key
|
49
|
-
=> 'foo_id'
|
50
|
-
|
51
|
-
### ensure it doesn't cause further migrations
|
52
|
-
|
53
|
-
doctest: check no further migrations
|
54
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
55
|
-
>> up
|
56
|
-
=> ""
|
@@ -1,846 +0,0 @@
|
|
1
|
-
# DeclareSchema - Migration Generator
|
2
|
-
|
3
|
-
Our test requires to prepare the testapp:
|
4
|
-
{.hidden}
|
5
|
-
|
6
|
-
doctest_require: 'prepare_testapp'
|
7
|
-
|
8
|
-
{.hidden}
|
9
|
-
|
10
|
-
## The migration generator -- introduction
|
11
|
-
|
12
|
-
The migration generator works by:
|
13
|
-
|
14
|
-
* Loading all of the models in your Rails app
|
15
|
-
* Using the Rails schema-dumper to extract information about the current state of the database.
|
16
|
-
* Calculating the changes that are required to bring the database into sync with your application.
|
17
|
-
|
18
|
-
Normally you would run the migration generator as a regular Rails generator. You would type
|
19
|
-
|
20
|
-
$ rails generate declare_schema:migration
|
21
|
-
|
22
|
-
in your Rails app, and the migration file would be created in `db/migrate`.
|
23
|
-
|
24
|
-
In order to demonstrate the generator in this doctest script however, we'll be using the Ruby API instead. The method `Generators::DeclareSchema::Migration::Migrator.run` returns a pair of strings -- the up migration and the down migration.
|
25
|
-
|
26
|
-
At the moment the database is empty and no ActiveRecord models exist, so the generator is going to tell us there is nothing to do.
|
27
|
-
|
28
|
-
>> Generators::DeclareSchema::Migration::Migrator.run
|
29
|
-
=> ["", ""]
|
30
|
-
|
31
|
-
|
32
|
-
### Models without `fields do` are ignored
|
33
|
-
|
34
|
-
The migration generator only takes into account classes that use DeclareSchema, i.e. classes with a `fields do` declaration. Models without this are ignored:
|
35
|
-
|
36
|
-
>> class Advert < ActiveRecord::Base; end
|
37
|
-
>> Generators::DeclareSchema::Migration::Migrator.run
|
38
|
-
=> ["", ""]
|
39
|
-
|
40
|
-
You can also tell DeclareSchema to ignore additional tables. You can place this command in your environment.rb or elsewhere:
|
41
|
-
|
42
|
-
>> Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
|
43
|
-
|
44
|
-
### Create the table
|
45
|
-
|
46
|
-
Here we see a simple `create_table` migration along with the `drop_table` down migration
|
47
|
-
|
48
|
-
>>
|
49
|
-
class Advert < ActiveRecord::Base
|
50
|
-
fields do
|
51
|
-
name :string, limit: 255, null: true
|
52
|
-
end
|
53
|
-
end
|
54
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
55
|
-
>> up
|
56
|
-
=>
|
57
|
-
"create_table :adverts do |t|
|
58
|
-
t.string :name
|
59
|
-
end"
|
60
|
-
>> down
|
61
|
-
=> "drop_table :adverts"
|
62
|
-
|
63
|
-
Normally we would run the generated migration with `rake db:create`. We can achieve the same effect directly in Ruby like this:
|
64
|
-
|
65
|
-
>> ActiveRecord::Migration.class_eval up
|
66
|
-
>> Advert.columns.map(&:name)
|
67
|
-
=> ["id", "name"]
|
68
|
-
|
69
|
-
We'll define a method to make that easier next time
|
70
|
-
|
71
|
-
>>
|
72
|
-
def migrate(renames={})
|
73
|
-
up, down = Generators::DeclareSchema::Migration::Migrator.run(renames)
|
74
|
-
ActiveRecord::Migration.class_eval(up)
|
75
|
-
ActiveRecord::Base.send(:descendants).each { |model| model.reset_column_information }
|
76
|
-
[up, down]
|
77
|
-
end
|
78
|
-
|
79
|
-
We'll have a look at the migration generator in more detail later, first we'll have a look at the extra features DeclareSchema has added to the model.
|
80
|
-
|
81
|
-
|
82
|
-
### Add fields
|
83
|
-
|
84
|
-
If we add a new field to the model, the migration generator will add it to the database.
|
85
|
-
|
86
|
-
>>
|
87
|
-
class Advert
|
88
|
-
fields do
|
89
|
-
name :string, limit: 255, null: true
|
90
|
-
body :text, null: true
|
91
|
-
published_at :datetime, null: true
|
92
|
-
end
|
93
|
-
end
|
94
|
-
>> up, down = migrate
|
95
|
-
>> up
|
96
|
-
=>
|
97
|
-
"add_column :adverts, :body, :text
|
98
|
-
add_column :adverts, :published_at, :datetime"
|
99
|
-
>> down
|
100
|
-
=>
|
101
|
-
"remove_column :adverts, :body
|
102
|
-
remove_column :adverts, :published_at"
|
103
|
-
>>
|
104
|
-
|
105
|
-
### Remove fields
|
106
|
-
|
107
|
-
If we remove a field from the model, the migration generator removes the database column. Note that we have to explicitly clear the known fields to achieve this in rdoctest -- in a Rails context you would simply edit the file
|
108
|
-
|
109
|
-
>> Advert.field_specs.clear # not normally needed
|
110
|
-
class Advert < ActiveRecord::Base
|
111
|
-
fields do
|
112
|
-
name :string, limit: 255, null: true
|
113
|
-
body :text, null: true
|
114
|
-
end
|
115
|
-
end
|
116
|
-
>> up, down = migrate
|
117
|
-
>> up
|
118
|
-
=> "remove_column :adverts, :published_at"
|
119
|
-
>> down
|
120
|
-
=> "add_column :adverts, :published_at, :datetime"
|
121
|
-
|
122
|
-
### Rename a field
|
123
|
-
|
124
|
-
Here we rename the `name` field to `title`. By default the generator sees this as removing `name` and adding `title`.
|
125
|
-
|
126
|
-
>> Advert.field_specs.clear # not normally needed
|
127
|
-
class Advert < ActiveRecord::Base
|
128
|
-
fields do
|
129
|
-
title :string, limit: 255, null: true
|
130
|
-
body :text, null: true
|
131
|
-
end
|
132
|
-
end
|
133
|
-
>> # Just generate - don't run the migration:
|
134
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
135
|
-
>> up
|
136
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
137
|
-
remove_column :adverts, :name"
|
138
|
-
>> down
|
139
|
-
=> "remove_column :adverts, :title
|
140
|
-
add_column :adverts, :name, :string, limit: 255"
|
141
|
-
|
142
|
-
When run as a generator, the migration-generator won't make this assumption. Instead it will prompt for user input to resolve the ambiguity. When using the Ruby API, we can ask for a rename instead of an add + drop by passing in a hash:
|
143
|
-
|
144
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })
|
145
|
-
>> up
|
146
|
-
=> "rename_column :adverts, :name, :title"
|
147
|
-
>> down
|
148
|
-
=> "rename_column :adverts, :title, :name"
|
149
|
-
|
150
|
-
Let's apply that change to the database
|
151
|
-
|
152
|
-
>> migrate
|
153
|
-
|
154
|
-
|
155
|
-
### Change a type
|
156
|
-
|
157
|
-
>>
|
158
|
-
class Advert
|
159
|
-
fields do
|
160
|
-
title :text, null: true
|
161
|
-
body :text, null: true
|
162
|
-
end
|
163
|
-
end
|
164
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
165
|
-
>> up
|
166
|
-
=> "change_column :adverts, :title, :text"
|
167
|
-
>> down
|
168
|
-
=> "change_column :adverts, :title, :string, limit: 255"
|
169
|
-
|
170
|
-
|
171
|
-
### Add a default
|
172
|
-
|
173
|
-
>>
|
174
|
-
class Advert
|
175
|
-
fields do
|
176
|
-
title :string, default: "Untitled", limit: 255, null: true
|
177
|
-
body :text, null: true
|
178
|
-
end
|
179
|
-
end
|
180
|
-
>> up, down = migrate
|
181
|
-
>> up.split(',').slice(0,3).join(',')
|
182
|
-
=> 'change_column :adverts, :title, :string'
|
183
|
-
>> up.split(',').slice(3,2).sort.join(',')
|
184
|
-
=> " default: \"Untitled\", limit: 255"
|
185
|
-
>> down
|
186
|
-
=> "change_column :adverts, :title, :string, limit: 255"
|
187
|
-
|
188
|
-
|
189
|
-
### Limits
|
190
|
-
|
191
|
-
>>
|
192
|
-
class Advert
|
193
|
-
fields do
|
194
|
-
price :integer, null: true, limit: 2
|
195
|
-
end
|
196
|
-
end
|
197
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
198
|
-
>> up
|
199
|
-
=> "add_column :adverts, :price, :integer, limit: 2"
|
200
|
-
|
201
|
-
Now run the migration, then change the limit:
|
202
|
-
|
203
|
-
>> ActiveRecord::Migration.class_eval up
|
204
|
-
>>
|
205
|
-
class Advert
|
206
|
-
fields do
|
207
|
-
price :integer, null: true, limit: 3
|
208
|
-
end
|
209
|
-
end
|
210
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
211
|
-
>> up
|
212
|
-
=> "change_column :adverts, :price, :integer, limit: 3"
|
213
|
-
>> down
|
214
|
-
=> "change_column :adverts, :price, :integer, limit: 2"
|
215
|
-
|
216
|
-
Note that limit on a decimal column is ignored (use :scale and :precision)
|
217
|
-
|
218
|
-
>> ActiveRecord::Migration.class_eval "remove_column :adverts, :price"
|
219
|
-
>>
|
220
|
-
class Advert
|
221
|
-
fields do
|
222
|
-
price :decimal, null: true, limit: 4
|
223
|
-
end
|
224
|
-
end
|
225
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
226
|
-
>> up
|
227
|
-
=> "add_column :adverts, :price, :decimal"
|
228
|
-
|
229
|
-
Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
|
230
|
-
allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
|
231
|
-
If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
|
232
|
-
that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
|
233
|
-
|
234
|
-
>> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
|
235
|
-
=> false
|
236
|
-
>>
|
237
|
-
class Advert
|
238
|
-
fields do
|
239
|
-
notes :text
|
240
|
-
description :text, limit: 30000
|
241
|
-
end
|
242
|
-
end
|
243
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
244
|
-
>> up
|
245
|
-
=> "add_column :adverts, :price, :decimal
|
246
|
-
add_column :adverts, :notes, :text, null: false
|
247
|
-
add_column :adverts, :description, :text, null: false"
|
248
|
-
|
249
|
-
(There is no limit on `add_column ... :description` above since these tests are run against SQLite.)
|
250
|
-
|
251
|
-
Cleanup
|
252
|
-
{.hidden}
|
253
|
-
>> Advert.field_specs.delete :price
|
254
|
-
>> Advert.field_specs.delete :notes
|
255
|
-
>> Advert.field_specs.delete :description
|
256
|
-
{.hidden}
|
257
|
-
|
258
|
-
In MySQL, limits are applied, rounded up:
|
259
|
-
|
260
|
-
>> ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
|
261
|
-
>> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
|
262
|
-
=> true
|
263
|
-
>>
|
264
|
-
class Advert
|
265
|
-
fields do
|
266
|
-
notes :text
|
267
|
-
description :text, limit: 200
|
268
|
-
end
|
269
|
-
end
|
270
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
271
|
-
>> up
|
272
|
-
=> "add_column :adverts, :notes, :text, null: false
|
273
|
-
add_column :adverts, :description, :text, null: false, limit: 255"
|
274
|
-
|
275
|
-
Cleanup
|
276
|
-
{.hidden}
|
277
|
-
|
278
|
-
>> Advert.field_specs.delete :notes
|
279
|
-
{.hidden}
|
280
|
-
|
281
|
-
Limits that are too high will for MySQL will raise an exception.
|
282
|
-
|
283
|
-
>> ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
|
284
|
-
>> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
|
285
|
-
=> true
|
286
|
-
>>
|
287
|
-
begin
|
288
|
-
class Advert
|
289
|
-
fields do
|
290
|
-
notes :text
|
291
|
-
description :text, limit: 0x1_0000_0000
|
292
|
-
end
|
293
|
-
end
|
294
|
-
rescue => ex
|
295
|
-
"#{ex.class}: #{ex.message}"
|
296
|
-
end
|
297
|
-
=> "ArgumentError: limit of 4294967296 is too large for MySQL"
|
298
|
-
|
299
|
-
Cleanup
|
300
|
-
{.hidden}
|
301
|
-
|
302
|
-
>> Advert.field_specs.delete :notes
|
303
|
-
{.hidden}
|
304
|
-
|
305
|
-
And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
|
306
|
-
|
307
|
-
To start, we'll set the database schema for `description` to match the above limit of 255.
|
308
|
-
|
309
|
-
>> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
|
310
|
-
=> true
|
311
|
-
>> Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
|
312
|
-
>> Advert.connection.schema_cache.clear!
|
313
|
-
>> Advert.reset_column_information
|
314
|
-
>> Advert.connection.tables
|
315
|
-
=> ["adverts"]
|
316
|
-
>> Advert.columns.map(&:name)
|
317
|
-
=> ["id", "body", "title", "description"]
|
318
|
-
|
319
|
-
Now migrate to an unstated text limit:
|
320
|
-
|
321
|
-
>>
|
322
|
-
class Advert
|
323
|
-
fields do
|
324
|
-
description :text
|
325
|
-
end
|
326
|
-
end
|
327
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
328
|
-
>> up
|
329
|
-
=> "change_column :adverts, :description, :text, null: false"
|
330
|
-
>> down
|
331
|
-
=> "change_column :adverts, :description, :text"
|
332
|
-
|
333
|
-
TODO TECH-4814: The above test should have this output:
|
334
|
-
TODO => "change_column :adverts, :description, :text, limit: 255"
|
335
|
-
|
336
|
-
|
337
|
-
And migrate to a stated text limit that is the same as the unstated one:
|
338
|
-
|
339
|
-
>>
|
340
|
-
class Advert
|
341
|
-
fields do
|
342
|
-
description :text, limit: 0xffffffff
|
343
|
-
end
|
344
|
-
end
|
345
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
346
|
-
>> up
|
347
|
-
=> "change_column :adverts, :description, :text, null: false"
|
348
|
-
>> down
|
349
|
-
=> "change_column :adverts, :description, :text"
|
350
|
-
>> ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, false)
|
351
|
-
|
352
|
-
Cleanup
|
353
|
-
{.hidden}
|
354
|
-
>> Advert.field_specs.clear
|
355
|
-
>> Advert.connection.schema_cache.clear!
|
356
|
-
>> Advert.reset_column_information
|
357
|
-
>>
|
358
|
-
class Advert < ActiveRecord::Base
|
359
|
-
fields do
|
360
|
-
name :string, limit: 255, null: true
|
361
|
-
end
|
362
|
-
end
|
363
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
364
|
-
>> ActiveRecord::Migration.class_eval up
|
365
|
-
>> Advert.connection.schema_cache.clear!
|
366
|
-
>> Advert.reset_column_information
|
367
|
-
{.hidden}
|
368
|
-
|
369
|
-
|
370
|
-
### Foreign Keys
|
371
|
-
|
372
|
-
DeclareSchema extends the `belongs_to` macro so that it also declares the
|
373
|
-
foreign-key field. It also generates an index on the field.
|
374
|
-
|
375
|
-
>>
|
376
|
-
class Category < ActiveRecord::Base; end
|
377
|
-
class Advert
|
378
|
-
belongs_to :category
|
379
|
-
end
|
380
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
381
|
-
>> up.gsub(/\n+/, "\n")
|
382
|
-
=> "add_column :adverts, :category_id, :integer, limit: 8, null: false
|
383
|
-
add_index :adverts, [:category_id], name: 'on_category_id'"
|
384
|
-
>> down.sub(/\n+/, "\n")
|
385
|
-
=> "remove_column :adverts, :category_id
|
386
|
-
remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid"
|
387
|
-
|
388
|
-
Cleanup:
|
389
|
-
{.hidden}
|
390
|
-
|
391
|
-
>> Advert.field_specs.delete(:category_id)
|
392
|
-
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
393
|
-
{.hidden}
|
394
|
-
|
395
|
-
If you specify a custom foreign key, the migration generator observes that:
|
396
|
-
|
397
|
-
>>
|
398
|
-
class Category < ActiveRecord::Base; end
|
399
|
-
class Advert
|
400
|
-
belongs_to :category, foreign_key: "c_id", class_name: 'Category'
|
401
|
-
end
|
402
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
403
|
-
>> up.gsub(/\n+/, "\n")
|
404
|
-
=> "add_column :adverts, :c_id, :integer, limit: 8, null: false
|
405
|
-
add_index :adverts, [:c_id], name: 'on_c_id'"
|
406
|
-
|
407
|
-
Cleanup:
|
408
|
-
{.hidden}
|
409
|
-
|
410
|
-
>> Advert.field_specs.delete(:c_id)
|
411
|
-
>> Advert.index_specs.delete_if { |spec| spec.fields==["c_id"] }
|
412
|
-
{.hidden}
|
413
|
-
|
414
|
-
You can avoid generating the index by specifying `index: false`
|
415
|
-
|
416
|
-
>>
|
417
|
-
class Category < ActiveRecord::Base; end
|
418
|
-
class Advert
|
419
|
-
belongs_to :category, index: false
|
420
|
-
end
|
421
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
422
|
-
>> up.gsub(/\n+/, "\n")
|
423
|
-
=> "add_column :adverts, :category_id, :integer, limit: 8, null: false"
|
424
|
-
|
425
|
-
Cleanup:
|
426
|
-
{.hidden}
|
427
|
-
|
428
|
-
>> Advert.field_specs.delete(:category_id)
|
429
|
-
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
430
|
-
{.hidden}
|
431
|
-
|
432
|
-
You can specify the index name with :index
|
433
|
-
|
434
|
-
>>
|
435
|
-
class Category < ActiveRecord::Base; end
|
436
|
-
class Advert
|
437
|
-
belongs_to :category, index: 'my_index'
|
438
|
-
end
|
439
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
440
|
-
>> up.gsub(/\n+/, "\n")
|
441
|
-
=> "add_column :adverts, :category_id, :integer, limit: 8, null: false
|
442
|
-
add_index :adverts, [:category_id], name: 'my_index'"
|
443
|
-
|
444
|
-
Cleanup:
|
445
|
-
{.hidden}
|
446
|
-
|
447
|
-
>> Advert.field_specs.delete(:category_id)
|
448
|
-
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
449
|
-
{.hidden}
|
450
|
-
|
451
|
-
### Timestamps and Optimimistic Locking
|
452
|
-
|
453
|
-
`updated_at` and `created_at` can be declared with the shorthand `timestamps`.
|
454
|
-
Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
|
455
|
-
|
456
|
-
>>
|
457
|
-
class Advert
|
458
|
-
fields do
|
459
|
-
timestamps
|
460
|
-
optimistic_lock
|
461
|
-
end
|
462
|
-
end
|
463
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
464
|
-
>> up.gsub(/\n+/, "\n")
|
465
|
-
=> "add_column :adverts, :created_at, :datetime
|
466
|
-
add_column :adverts, :updated_at, :datetime
|
467
|
-
add_column :adverts, :lock_version, :integer, null: false, default: 1"
|
468
|
-
>> down.gsub(/\n+/, "\n")
|
469
|
-
=> "remove_column :adverts, :created_at
|
470
|
-
remove_column :adverts, :updated_at
|
471
|
-
remove_column :adverts, :lock_version"
|
472
|
-
>>
|
473
|
-
|
474
|
-
Cleanup:
|
475
|
-
{.hidden}
|
476
|
-
|
477
|
-
>> Advert.field_specs.delete(:updated_at)
|
478
|
-
>> Advert.field_specs.delete(:created_at)
|
479
|
-
>> Advert.field_specs.delete(:lock_version)
|
480
|
-
{.hidden}
|
481
|
-
|
482
|
-
### Indices
|
483
|
-
|
484
|
-
You can add an index to a field definition
|
485
|
-
|
486
|
-
>>
|
487
|
-
class Advert
|
488
|
-
fields do
|
489
|
-
title :string, index: true, limit: 255, null: true
|
490
|
-
end
|
491
|
-
end
|
492
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
493
|
-
>> up.gsub(/\n+/, "\n")
|
494
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
495
|
-
add_index :adverts, [:title], name: 'on_title'"
|
496
|
-
|
497
|
-
Cleanup:
|
498
|
-
{.hidden}
|
499
|
-
|
500
|
-
>> Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
|
501
|
-
{.hidden}
|
502
|
-
|
503
|
-
You can ask for a unique index
|
504
|
-
|
505
|
-
>>
|
506
|
-
class Advert
|
507
|
-
fields do
|
508
|
-
title :string, index: true, unique: true, null: true, limit: 255
|
509
|
-
end
|
510
|
-
end
|
511
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
512
|
-
>> up.gsub(/\n+/, "\n")
|
513
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
514
|
-
add_index :adverts, [:title], unique: true, name: 'on_title'"
|
515
|
-
|
516
|
-
Cleanup:
|
517
|
-
{.hidden}
|
518
|
-
|
519
|
-
>> Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
|
520
|
-
{.hidden}
|
521
|
-
|
522
|
-
You can specify the name for the index
|
523
|
-
|
524
|
-
>>
|
525
|
-
class Advert
|
526
|
-
fields do
|
527
|
-
title :string, index: 'my_index', limit: 255, null: true
|
528
|
-
end
|
529
|
-
end
|
530
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
531
|
-
>> up.gsub(/\n+/, "\n")
|
532
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
533
|
-
add_index :adverts, [:title], name: 'my_index'"
|
534
|
-
|
535
|
-
Cleanup:
|
536
|
-
{.hidden}
|
537
|
-
|
538
|
-
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
539
|
-
{.hidden}
|
540
|
-
|
541
|
-
You can ask for an index outside of the fields block
|
542
|
-
|
543
|
-
>>
|
544
|
-
class Advert
|
545
|
-
index :title
|
546
|
-
end
|
547
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
548
|
-
>> up.gsub(/\n+/, "\n")
|
549
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
550
|
-
add_index :adverts, [:title], name: 'on_title'"
|
551
|
-
|
552
|
-
Cleanup:
|
553
|
-
{.hidden}
|
554
|
-
|
555
|
-
>> Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
|
556
|
-
{.hidden}
|
557
|
-
|
558
|
-
The available options for the index function are `:unique` and `:name`
|
559
|
-
|
560
|
-
>>
|
561
|
-
class Advert
|
562
|
-
index :title, unique: true, name: 'my_index'
|
563
|
-
end
|
564
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
565
|
-
>> up.gsub(/\n+/, "\n")
|
566
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
567
|
-
add_index :adverts, [:title], unique: true, name: 'my_index'"
|
568
|
-
|
569
|
-
Cleanup:
|
570
|
-
{.hidden}
|
571
|
-
|
572
|
-
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
573
|
-
{.hidden}
|
574
|
-
|
575
|
-
You can create an index on more than one field
|
576
|
-
|
577
|
-
>>
|
578
|
-
class Advert
|
579
|
-
index [:title, :category_id]
|
580
|
-
end
|
581
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
582
|
-
>> up.gsub(/\n+/, "\n")
|
583
|
-
=> "add_column :adverts, :title, :string, limit: 255
|
584
|
-
add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'"
|
585
|
-
|
586
|
-
Cleanup:
|
587
|
-
{.hidden}
|
588
|
-
|
589
|
-
>> Advert.index_specs.delete_if { |spec| spec.fields==["title", "category_id"] }
|
590
|
-
{.hidden}
|
591
|
-
|
592
|
-
Finally, you can specify that the migration generator should completely ignore an index by passing its name to ignore_index in the model. This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
|
593
|
-
|
594
|
-
### Rename a table
|
595
|
-
|
596
|
-
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.
|
597
|
-
|
598
|
-
>>
|
599
|
-
class Advert
|
600
|
-
self.table_name="ads"
|
601
|
-
fields do
|
602
|
-
title :string, limit: 255, null: true
|
603
|
-
body :text, null: true
|
604
|
-
end
|
605
|
-
end
|
606
|
-
|
607
|
-
>> Advert.connection.schema_cache.clear!
|
608
|
-
>> Advert.reset_column_information
|
609
|
-
|
610
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")
|
611
|
-
>> up.gsub(/\n+/, "\n")
|
612
|
-
=> "rename_table :adverts, :ads
|
613
|
-
add_column :ads, :title, :string, limit: 255
|
614
|
-
add_column :ads, :body, :text
|
615
|
-
add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'"
|
616
|
-
>> down.gsub(/\n+/, "\n")
|
617
|
-
=> "remove_column :ads, :title
|
618
|
-
remove_column :ads, :body
|
619
|
-
rename_table :ads, :adverts
|
620
|
-
add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'"
|
621
|
-
|
622
|
-
Set the table name back to what it should be and confirm we're in sync:
|
623
|
-
|
624
|
-
>> Advert.field_specs.delete(:title)
|
625
|
-
>> Advert.field_specs.delete(:body)
|
626
|
-
>> class Advert; self.table_name="adverts"; end
|
627
|
-
>> Generators::DeclareSchema::Migration::Migrator.run
|
628
|
-
=> ["", ""]
|
629
|
-
|
630
|
-
### Rename a table
|
631
|
-
|
632
|
-
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.
|
633
|
-
{.hidden}
|
634
|
-
|
635
|
-
>>
|
636
|
-
def nuke_model_class(klass)
|
637
|
-
ActiveSupport::DescendantsTracker.instance_eval do
|
638
|
-
direct_descendants = class_variable_get('@@direct_descendants')
|
639
|
-
direct_descendants[ActiveRecord::Base] = direct_descendants[ActiveRecord::Base].to_a.reject { |descendant| descendant == klass }
|
640
|
-
end
|
641
|
-
Object.instance_eval { remove_const klass.name.to_sym }
|
642
|
-
end
|
643
|
-
>> nuke_model_class(Advert)
|
644
|
-
{.hidden}
|
645
|
-
|
646
|
-
>>
|
647
|
-
class Advertisement < ActiveRecord::Base
|
648
|
-
fields do
|
649
|
-
title :string, limit: 255, null: true
|
650
|
-
body :text, null: true
|
651
|
-
end
|
652
|
-
end
|
653
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")
|
654
|
-
>> up.gsub(/\n+/, "\n")
|
655
|
-
=> "rename_table :adverts, :advertisements
|
656
|
-
add_column :advertisements, :title, :string, limit: 255
|
657
|
-
add_column :advertisements, :body, :text
|
658
|
-
remove_column :advertisements, :name
|
659
|
-
add_index :advertisements, [:id], unique: true, name: 'PRIMARY_KEY'"
|
660
|
-
>> down.gsub(/\n+/, "\n")
|
661
|
-
=> "remove_column :advertisements, :title
|
662
|
-
remove_column :advertisements, :body
|
663
|
-
add_column :adverts, :name, :string, limit: 255
|
664
|
-
rename_table :advertisements, :adverts
|
665
|
-
add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'"
|
666
|
-
|
667
|
-
### Drop a table
|
668
|
-
|
669
|
-
>> nuke_model_class(Advertisement)
|
670
|
-
{.hidden}
|
671
|
-
|
672
|
-
If you delete a model, the migration generator will create a `drop_table` migration.
|
673
|
-
|
674
|
-
Dropping tables is where the automatic down-migration really comes in handy:
|
675
|
-
|
676
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
677
|
-
>> up
|
678
|
-
=> "drop_table :adverts"
|
679
|
-
>> down.gsub(/,.*/m, '')
|
680
|
-
=> "create_table \"adverts\""
|
681
|
-
|
682
|
-
## STI
|
683
|
-
|
684
|
-
### Adding an STI subclass
|
685
|
-
|
686
|
-
Adding a subclass or two should introduce the 'type' column and no other changes
|
687
|
-
|
688
|
-
>>
|
689
|
-
class Advert < ActiveRecord::Base
|
690
|
-
fields do
|
691
|
-
body :text, null: true
|
692
|
-
title :string, default: "Untitled", limit: 255, null: true
|
693
|
-
end
|
694
|
-
end
|
695
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
696
|
-
>> ActiveRecord::Migration.class_eval up
|
697
|
-
|
698
|
-
class FancyAdvert < Advert
|
699
|
-
end
|
700
|
-
class SuperFancyAdvert < FancyAdvert
|
701
|
-
end
|
702
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
703
|
-
>> up.gsub(/\n+/, "\n")
|
704
|
-
=> "add_column :adverts, :type, :string, limit: 255
|
705
|
-
add_index :adverts, [:type], name: 'on_type'"
|
706
|
-
>> down.gsub(/\n+/, "\n")
|
707
|
-
=> "remove_column :adverts, :type
|
708
|
-
remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid"
|
709
|
-
|
710
|
-
Cleanup
|
711
|
-
{.hidden}
|
712
|
-
|
713
|
-
>> Advert.field_specs.delete(:type)
|
714
|
-
>> nuke_model_class(SuperFancyAdvert)
|
715
|
-
>> nuke_model_class(FancyAdvert)
|
716
|
-
>> Advert.index_specs.delete_if { |spec| spec.fields==["type"] }
|
717
|
-
{.hidden}
|
718
|
-
|
719
|
-
|
720
|
-
## Coping with multiple changes
|
721
|
-
|
722
|
-
The migration generator is designed to create complete migrations even if many changes to the models have taken place.
|
723
|
-
|
724
|
-
First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
|
725
|
-
|
726
|
-
>> ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
|
727
|
-
>> Advert.connection.schema_cache.clear!
|
728
|
-
>> Advert.reset_column_information
|
729
|
-
|
730
|
-
>> Advert.connection.tables
|
731
|
-
=> ["adverts"]
|
732
|
-
>> Advert.columns.map(&:name).sort
|
733
|
-
=> ["body", "id", "title"]
|
734
|
-
>> Generators::DeclareSchema::Migration::Migrator.run
|
735
|
-
=> ["", ""]
|
736
|
-
|
737
|
-
|
738
|
-
### Rename a column and change the default
|
739
|
-
|
740
|
-
>> Advert.field_specs.clear
|
741
|
-
>>
|
742
|
-
class Advert
|
743
|
-
fields do
|
744
|
-
name :string, default: "No Name", limit: 255, null: true
|
745
|
-
body :text, null: true
|
746
|
-
end
|
747
|
-
end
|
748
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })
|
749
|
-
>> up
|
750
|
-
=> "rename_column :adverts, :title, :name
|
751
|
-
change_column :adverts, :name, :string, limit: 255, default: \"No Name\""
|
752
|
-
>> down
|
753
|
-
=> "rename_column :adverts, :name, :title
|
754
|
-
change_column :adverts, :title, :string, limit: 255, default: \"Untitled\""
|
755
|
-
|
756
|
-
|
757
|
-
### Rename a table and add a column
|
758
|
-
|
759
|
-
>> nuke_model_class(Advert)
|
760
|
-
{.hidden}
|
761
|
-
|
762
|
-
>>
|
763
|
-
class Ad < ActiveRecord::Base
|
764
|
-
fields do
|
765
|
-
title :string, default: "Untitled", limit: 255
|
766
|
-
body :text, null: true
|
767
|
-
created_at :datetime
|
768
|
-
end
|
769
|
-
end
|
770
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)
|
771
|
-
>> up.gsub(/\n+/, "\n")
|
772
|
-
=> "rename_table :adverts, :ads
|
773
|
-
add_column :ads, :created_at, :datetime, null: false
|
774
|
-
change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
|
775
|
-
add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'"
|
776
|
-
|
777
|
-
>>
|
778
|
-
class Advert < ActiveRecord::Base
|
779
|
-
fields do
|
780
|
-
body :text, null: true
|
781
|
-
title :string, default: "Untitled", limit: 255, null: true
|
782
|
-
end
|
783
|
-
end
|
784
|
-
{.hidden}
|
785
|
-
|
786
|
-
## Legacy Keys
|
787
|
-
|
788
|
-
DeclareSchema has some support for legacy keys.
|
789
|
-
|
790
|
-
>> nuke_model_class(Ad)
|
791
|
-
>>
|
792
|
-
class Advert < ActiveRecord::Base
|
793
|
-
fields do
|
794
|
-
body :text, null: true
|
795
|
-
end
|
796
|
-
self.primary_key="advert_id"
|
797
|
-
end
|
798
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })
|
799
|
-
>> up.gsub(/\n+/, "\n")
|
800
|
-
=> "rename_column :adverts, :id, :advert_id
|
801
|
-
add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY_KEY'"
|
802
|
-
|
803
|
-
>> nuke_model_class(Advert)
|
804
|
-
>> ActiveRecord::Base.connection.execute "drop table `adverts`;"
|
805
|
-
{.hidden}
|
806
|
-
|
807
|
-
## DSL
|
808
|
-
|
809
|
-
The DSL allows lambdas and constants
|
810
|
-
|
811
|
-
>>
|
812
|
-
class User < ActiveRecord::Base
|
813
|
-
fields do
|
814
|
-
company :string, limit: 255, ruby_default: -> { "BigCorp" }
|
815
|
-
end
|
816
|
-
end
|
817
|
-
>> User.field_specs.keys
|
818
|
-
=> ['company']
|
819
|
-
>> User.field_specs['company'].options[:ruby_default]&.call
|
820
|
-
=> "BigCorp"
|
821
|
-
|
822
|
-
|
823
|
-
## validates
|
824
|
-
|
825
|
-
DeclareSchema can accept a validates hash in the field options.
|
826
|
-
|
827
|
-
>> $company_validates_options = :none
|
828
|
-
>>
|
829
|
-
class Ad < ActiveRecord::Base; end;
|
830
|
-
def Ad.validates(field_name, options)
|
831
|
-
$company_validates_options = "got field_name: #{field_name}, options: #{options.inspect}"
|
832
|
-
end
|
833
|
-
>>
|
834
|
-
class Ad < ActiveRecord::Base
|
835
|
-
fields do
|
836
|
-
company :string, limit: 255, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
|
837
|
-
end
|
838
|
-
self.primary_key="advert_id"
|
839
|
-
end
|
840
|
-
>> # expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
|
841
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
842
|
-
>> ActiveRecord::Migration.class_eval up
|
843
|
-
>> $company_validates_options
|
844
|
-
=> "got field_name: company, options: {:presence=>true, :uniqueness=>{:case_sensitive=>false}}"
|
845
|
-
>> Ad.field_specs['company'].options[:validates].inspect
|
846
|
-
=> "{:presence=>true, :uniqueness=>{:case_sensitive=>false}}"
|