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.
@@ -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}}"