declare_schema 0.1.3 → 0.2.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.3
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-08 00:00:00.000000000 Z
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? -%>
@@ -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
@@ -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}}"