declare_schema 1.3.2.rp.1 → 1.4.0.colin.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 +7 -1
- data/Gemfile.lock +1 -1
- data/README.md +82 -38
- data/lib/declare_schema/model/index_definition.rb +18 -5
- data/lib/declare_schema/model.rb +29 -7
- data/lib/declare_schema/schema_change/index_add.rb +3 -1
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +1 -1
- data/spec/lib/declare_schema/migration_generator_spec.rb +80 -51
- data/spec/lib/declare_schema/model/index_definition_spec.rb +103 -27
- data/spec/lib/declare_schema/schema_change/index_add_spec.rb +28 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe0a594fea50536d529c2d4e42280d9050726e1017524dbaff2b8a77e17c6b41
|
4
|
+
data.tar.gz: 89199ba5b58b936f93a7bfe0409c403b95da8bd771b425b2c61a6afd8a951f2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff4858ac26953e22a9dbf37ce5b2391295d05ec5a8401361434987e6e90c7fc01543f2bbb1cebaf91a0ce16dadb8ac5c8600d01130c81f361c3728bcd7c23f9b
|
7
|
+
data.tar.gz: 1fabf741327417f5beb76ba694de17e0176db5f63e95922a03d195eb4dcff60d71434cd6aeb4e8ff8212bc0b1fc7cc5a8cb400307efeb87a83593887621d366b
|
data/CHANGELOG.md
CHANGED
@@ -4,7 +4,13 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
4
4
|
|
5
5
|
Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
-
## [1.
|
7
|
+
## [1.4] - Unreleased
|
8
|
+
### Added
|
9
|
+
- Added support for partial indexes with `length:` option.
|
10
|
+
### Changed
|
11
|
+
- Deprecate index: 'name' and unique: true|false in favor of index: { name: 'name', unique: true|false }.
|
12
|
+
|
13
|
+
## [1.3.2] - 2024-01-12
|
8
14
|
### Fixed
|
9
15
|
- Fix bug in migrator when table option definitions differ
|
10
16
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,7 @@ Make a model and declare your schema within a `declare_schema do ... end` block:
|
|
9
9
|
class Company < ActiveRecord::Base
|
10
10
|
declare_schema do
|
11
11
|
string :company_name, limit: 100
|
12
|
-
string :ticker_symbol, limit: 4, null: true, index:
|
12
|
+
string :ticker_symbol, limit: 4, null: true, index: { unique: true }
|
13
13
|
integer :employee_count
|
14
14
|
text :comments
|
15
15
|
|
@@ -60,6 +60,61 @@ declare_schema id: :bigint do
|
|
60
60
|
end
|
61
61
|
```
|
62
62
|
|
63
|
+
## declare_schema DSL field (column) declaration
|
64
|
+
The `declare_schema` DSL is yielded to the block as shown with block variable `t` (for table).
|
65
|
+
Each field (column) is declared with the syntax `t.<type> :<column_name>, <options>` as shown here for the `string` column `company_name`:
|
66
|
+
```ruby
|
67
|
+
create_table :companies, id: :bigint do |t|
|
68
|
+
t.string :company_name, null: false, limit: 100
|
69
|
+
...
|
70
|
+
end
|
71
|
+
```
|
72
|
+
### Field (Column) Types
|
73
|
+
All of the ActiveRecord field types are supported, as returned by the database driver in use at the time.
|
74
|
+
These typically include:
|
75
|
+
- `binary` (blob)
|
76
|
+
- `text`
|
77
|
+
- `integer`
|
78
|
+
- `bigint`
|
79
|
+
- `float`
|
80
|
+
- `decimal`
|
81
|
+
- `date`
|
82
|
+
- `time`
|
83
|
+
- `datetime`
|
84
|
+
- `timestamp`
|
85
|
+
- `string` (varchar)
|
86
|
+
- `boolean` (tinyint 0 or 1)
|
87
|
+
- `json`
|
88
|
+
- `array`
|
89
|
+
- `enum` (if using the `activerecord-mysql-enum` gem) (MySQL enum)
|
90
|
+
|
91
|
+
### Field (Column) Options
|
92
|
+
The following field options are:
|
93
|
+
- `limit` (integer) - The maximum length of the field. For `text` and `binary` fields, this is the maximum number of bytes.
|
94
|
+
For `string` fields, this is the maximum number of characters, and defaults to `DeclareSchema.default_string_limit`; for `text`, defaults to `DeclareSchema.default_text_limit`.
|
95
|
+
For `enum`
|
96
|
+
- `null` (boolean) - Whether the field is nullable. Defaults to `DeclareSchema.default_null`.
|
97
|
+
- `default` (any) - The default value for the field.
|
98
|
+
- `ruby_default` (Proc) - A callable Ruby Proc that returns the default value for the field. This is useful for default values that require Ruby computation.
|
99
|
+
(Provided by the `attr_default` gem.)
|
100
|
+
- `index` (boolean [deprecated] or hash) - Whether to create an index for the field. If `true`, defaults to `{ unique: false }` [deprecated]. See below for supported `index` options.
|
101
|
+
- `unique` [deprecated] (boolean) - Whether to create a unique index for the field. Defaults to `false`. Deprecated in favor of `index: { unique: <boolean> }`.
|
102
|
+
- `charset` (string) - The character set for the field. Defaults to `default_charset` (see below).
|
103
|
+
- `collation` (string) - The collation for the field. Defaults to `default_collation` (see below).
|
104
|
+
- `precision` (integer) - The precision for the numeric field.
|
105
|
+
- `scale` (integer) - The scale for the numeric field.
|
106
|
+
|
107
|
+
### Index Options
|
108
|
+
The following `index` options are supported:
|
109
|
+
- `name` (string) - The name of the index. Defaults the longest format that will fit within `DeclareSchema.max_index_and_constraint_name_length`. They are tried in this order:
|
110
|
+
1. `index_<table>_on_<col1>[_and_<col2>...]>`.
|
111
|
+
2. `__<col1>[_<col2>...]>`
|
112
|
+
3. `<table_prefix><sha256_of_columns_prefix>`
|
113
|
+
- `unique` (boolean) - Whether the index is unique. Defaults to `false`.
|
114
|
+
- `order` (synbol or hash) - The index order. If `:asc` or `:desc` is provided, it is used as the order for all columns. If hash is provided, it is used to specify the order of individual columns, where the column names are given as `Symbol` hash keys with values of `:asc` or `:desc` indicating the sort order of that column.
|
115
|
+
- `length` (integer or hash) - The partial index length(s). If an integer is provided, it is used as the length for all columns. If a hash is provided, it is used to specify the length for individual columns, where the column names are given as `Symbol` hash keys.
|
116
|
+
- `where` (string) - The subset index predicate.
|
117
|
+
|
63
118
|
## Usage without Rails
|
64
119
|
|
65
120
|
When using `DeclareSchema` without Rails, you can use the `declare_schema/rake` task to generate the migration file.
|
@@ -115,13 +170,13 @@ end
|
|
115
170
|
```
|
116
171
|
|
117
172
|
### clear_default_schema
|
118
|
-
This method clears out any previously declared `default_schema`.
|
173
|
+
This method clears out any previously declared `default_schema`. This can be useful for tests.
|
119
174
|
```ruby
|
120
175
|
DeclareSchema.clear_default_schema
|
121
176
|
```
|
122
177
|
|
123
178
|
### Global Configuration
|
124
|
-
Configurations can be set at
|
179
|
+
Configurations can be set at globally to customize default declaration for the following values:
|
125
180
|
|
126
181
|
#### Text Limit
|
127
182
|
The default text limit can be set using the `DeclareSchema.default_text_limit=` method.
|
@@ -134,8 +189,6 @@ set the default `text limit` value to `0xffff`:
|
|
134
189
|
|
135
190
|
**declare_schema.rb**
|
136
191
|
```ruby
|
137
|
-
# frozen_string_literal: true
|
138
|
-
|
139
192
|
DeclareSchema.default_text_limit = 0xffff
|
140
193
|
```
|
141
194
|
|
@@ -150,8 +203,6 @@ set the default `string limit` value to `255`:
|
|
150
203
|
|
151
204
|
**declare_schema.rb**
|
152
205
|
```ruby
|
153
|
-
# frozen_string_literal: true
|
154
|
-
|
155
206
|
DeclareSchema.default_string_limit = 255
|
156
207
|
```
|
157
208
|
|
@@ -166,40 +217,34 @@ set the default `null` value to `true`:
|
|
166
217
|
|
167
218
|
**declare_schema.rb**
|
168
219
|
```ruby
|
169
|
-
# frozen_string_literal: true
|
170
|
-
|
171
220
|
DeclareSchema.default_null = true
|
172
221
|
```
|
173
222
|
|
174
223
|
#### Generate Foreign Keys
|
175
|
-
|
176
|
-
This
|
224
|
+
You can choose whether to generate foreign keys by using the `DeclareSchema.default_generate_foreign_keys=` method.
|
225
|
+
This defaults to `true` and can only be set globally.
|
177
226
|
|
178
|
-
For example, adding the following to your `config/initializers` directory will
|
179
|
-
|
227
|
+
For example, adding the following to your `config/initializers` directory will cause
|
228
|
+
foreign keys not to be generated:
|
180
229
|
|
181
230
|
**declare_schema.rb**
|
182
231
|
```ruby
|
183
|
-
# frozen_string_literal: true
|
184
|
-
|
185
232
|
DeclareSchema.default_generate_foreign_keys = false
|
186
233
|
```
|
187
234
|
|
188
235
|
#### Generate Indexing
|
189
|
-
|
190
|
-
This
|
236
|
+
You can choose whether to generate indexes automatically by using the `DeclareSchema.default_generate_indexing=` method.
|
237
|
+
This defaults to `true` and can only be set globally.
|
191
238
|
|
192
|
-
For example, adding the following to your `config/initializers` directory will
|
193
|
-
|
239
|
+
For example, adding the following to your `config/initializers` directory will cause
|
240
|
+
indexes not to be generated by `declare_schema`:
|
194
241
|
|
195
242
|
**declare_schema.rb**
|
196
243
|
```ruby
|
197
|
-
# frozen_string_literal: true
|
198
|
-
|
199
244
|
DeclareSchema.default_generate_indexing = false
|
200
245
|
```
|
201
246
|
#### Character Set and Collation
|
202
|
-
The character set and collation for all tables and fields can be set at
|
247
|
+
The character set and collation for all tables and fields can be set at globally
|
203
248
|
using the `Generators::DeclareSchema::Migrator.default_charset=` and
|
204
249
|
`Generators::DeclareSchema::Migrator.default_collation=` configuration methods.
|
205
250
|
|
@@ -208,8 +253,6 @@ turn all tables into `utf8mb4` supporting tables:
|
|
208
253
|
|
209
254
|
**declare_schema.rb**
|
210
255
|
```ruby
|
211
|
-
# frozen_string_literal: true
|
212
|
-
|
213
256
|
DeclareSchema.default_charset = "utf8mb4"
|
214
257
|
DeclareSchema.default_collation = "utf8mb4_bin"
|
215
258
|
```
|
@@ -221,7 +264,7 @@ bundle exec rails db:migrate
|
|
221
264
|
```
|
222
265
|
If your repo has a different command to run for migrations, you can configure it like this:
|
223
266
|
```ruby
|
224
|
-
|
267
|
+
DeclareSchema.db_migrate_command = "bundle exec rails db:migrate_immediate"
|
225
268
|
```
|
226
269
|
|
227
270
|
## The `belongs_to` Association
|
@@ -230,24 +273,16 @@ association is outside of the `declare_schema do` block, so `declare_schema` int
|
|
230
273
|
infer the foreign key column.
|
231
274
|
|
232
275
|
By default, `declare_schema` creates an index for `belongs_to` relations. If this default index is not desired,
|
233
|
-
you can use `index: false` in the `belongs_to` expression. This may be the case if for example a different index
|
234
|
-
already
|
276
|
+
you can use `index: false` in the `belongs_to` expression. This may be the case if, for example, a different index
|
277
|
+
already covers those columns at the front.
|
235
278
|
|
236
279
|
## The `has_and_belongs_to_many` Association
|
237
280
|
Like the `belongs_to` association, `has_and_belongs_to_many` is outside of the `declare_schema ` block. `declare_schema` similarly
|
238
281
|
infers foreign keys (and the intersection table).
|
239
282
|
|
240
283
|
## Ignored Tables
|
241
|
-
If a table's schema or metadata are managed elsewhere, `declare_schema`
|
242
|
-
|
243
|
-
|
244
|
-
`declare_schema` by default ignores these tables:
|
245
|
-
- The ActiveRecord `schema_info` table
|
246
|
-
- The ActiveRecord schema migrations table (generally named `schema_migrations`)
|
247
|
-
- The ActiveRecord internal metadata table (generally named `ar_internal_metadata`)
|
248
|
-
- If defined/configured, the CGI ActiveRecordStore session table
|
249
|
-
|
250
|
-
Additional tables can be ignored by configuring `Generators::DeclareSchema::Migration::Migrator.ignore_tables`.
|
284
|
+
If a table's schema or metadata are managed elsewhere, `declare_schema` can be instructed to ignore it
|
285
|
+
by adding those table names to the array assigned to `Generators::DeclareSchema::Migration::Migrator.ignore_tables`.
|
251
286
|
For example:
|
252
287
|
|
253
288
|
```ruby
|
@@ -258,6 +293,12 @@ For example:
|
|
258
293
|
]
|
259
294
|
```
|
260
295
|
|
296
|
+
Note: `declare_schema` always ignores these tables:
|
297
|
+
- The ActiveRecord `schema_info` table
|
298
|
+
- The ActiveRecord schema migrations table (generally named `schema_migrations`)
|
299
|
+
- The ActiveRecord internal metadata table (generally named `ar_internal_metadata`)
|
300
|
+
- If defined/configured, the CGI ActiveRecordStore session table
|
301
|
+
|
261
302
|
## Maximum Length of Index and Constraint Names
|
262
303
|
|
263
304
|
MySQL limits the length of index and constraint names to 64 characters.
|
@@ -279,7 +320,10 @@ But later, Unicode was extended beyond U+FFFF to make room for emojis, and with
|
|
279
320
|
UTF-8 require 1-4 bytes (`mb4` or "multi-byte 4"). With this addition, there has
|
280
321
|
come a need to dynamically define the character set and collation for individual
|
281
322
|
tables and columns in the database. With `declare_schema` this can be configured
|
282
|
-
at three separate levels
|
323
|
+
at three separate levels.
|
324
|
+
|
325
|
+
### Global Configuration
|
326
|
+
The global configuration option is explained above in the [Character Set and Collation](#Character-Set-and-Collation) section.
|
283
327
|
|
284
328
|
### Table Configuration
|
285
329
|
In order to configure a table's default character set and collation, the `charset` and
|
@@ -8,7 +8,8 @@ module DeclareSchema
|
|
8
8
|
include Comparable
|
9
9
|
|
10
10
|
# TODO: replace `fields` with `columns` and remove alias. -Colin
|
11
|
-
|
11
|
+
OPTIONS = [:name, :unique, :where, :length].freeze
|
12
|
+
attr_reader :table, :fields, :explicit_name, *OPTIONS
|
12
13
|
alias columns fields
|
13
14
|
|
14
15
|
class IndexNameTooLongError < RuntimeError; end
|
@@ -22,14 +23,17 @@ module DeclareSchema
|
|
22
23
|
@explicit_name = options[:name] unless options.delete(:allow_equivalent)
|
23
24
|
@name = options.delete(:name) || self.class.default_index_name(@table, @fields)
|
24
25
|
@unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
|
26
|
+
@length = options.delete(:length)
|
25
27
|
|
26
28
|
if DeclareSchema.max_index_and_constraint_name_length && @name.length > DeclareSchema.max_index_and_constraint_name_length
|
27
29
|
raise IndexNameTooLongError, "Index '#{@name}' exceeds configured limit of #{DeclareSchema.max_index_and_constraint_name_length} characters. Give it a shorter name, or adjust DeclareSchema.max_index_and_constraint_name_length if you know your database can accept longer names."
|
28
30
|
end
|
29
31
|
|
30
|
-
if (where = options
|
32
|
+
if (where = options.delete(:where))
|
31
33
|
@where = where.start_with?('(') ? where : "(#{where})"
|
32
34
|
end
|
35
|
+
|
36
|
+
options.any? and warn("ignoring unrecognized option(s): #{options.inspect} for model #{model}")
|
33
37
|
end
|
34
38
|
|
35
39
|
class << self
|
@@ -102,12 +106,21 @@ module DeclareSchema
|
|
102
106
|
name == PRIMARY_KEY_NAME
|
103
107
|
end
|
104
108
|
|
109
|
+
def options
|
110
|
+
@options ||=
|
111
|
+
OPTIONS.each_with_object({}) do |option, result|
|
112
|
+
result[option] = send(option)
|
113
|
+
end.freeze
|
114
|
+
end
|
115
|
+
|
116
|
+
# Unique key for this object. Used for equality checking.
|
105
117
|
def to_key
|
106
|
-
@key ||= [table, fields,
|
118
|
+
@key ||= [table, fields, options].freeze
|
107
119
|
end
|
108
120
|
|
121
|
+
# The index settings for this object. Used for equivalence checking. Does not include the name.
|
109
122
|
def settings
|
110
|
-
@settings ||= [table, fields,
|
123
|
+
@settings ||= [table, fields, options.except(:name)].freeze
|
111
124
|
end
|
112
125
|
|
113
126
|
def hash
|
@@ -123,7 +136,7 @@ module DeclareSchema
|
|
123
136
|
end
|
124
137
|
|
125
138
|
def with_name(new_name)
|
126
|
-
self.class.new(@model, @fields,
|
139
|
+
self.class.new(@model, @fields, **{ **options, name: new_name })
|
127
140
|
end
|
128
141
|
|
129
142
|
alias eql? ==
|
data/lib/declare_schema/model.rb
CHANGED
@@ -114,14 +114,36 @@ module DeclareSchema
|
|
114
114
|
ActiveSupport::Deprecation.warn("belongs_to limit: is deprecated since it is now inferred")
|
115
115
|
end
|
116
116
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
117
|
+
# index: true means create an index on the foreign key
|
118
|
+
# index: false means do not create an index on the foreign key
|
119
|
+
# index: { ... } means create an index on the foreign key with the given options
|
120
|
+
index_value = options.delete(:index)
|
121
|
+
if index_value != false || options.has_key?(:unique) || options.has_key?(:allow_equivalent)
|
122
|
+
index_options = {}
|
123
|
+
case index_value
|
124
|
+
when String
|
125
|
+
Kernel.warn("belongs_to index: 'name' is deprecated; use index: { name: 'name' } instead")
|
126
|
+
index_options[:name] = index_value
|
127
|
+
# when false -- impossible since we checked that above
|
128
|
+
when true
|
129
|
+
when nil
|
130
|
+
when Hash
|
131
|
+
index_options = index_value
|
132
|
+
else
|
133
|
+
raise ArgumentError, "belongs_to index: must be true or false or a Hash; got #{index_value.inspect}"
|
134
|
+
end
|
135
|
+
|
136
|
+
if options.has_key?(:unique)
|
137
|
+
Kernel.warn("belongs_to unique: true|false is deprecated; use index: { unique: true|false } instead")
|
138
|
+
index_options[:unique] = options.delete(:unique)
|
139
|
+
end
|
140
|
+
|
141
|
+
index_options[:allow_equivalent] = options.delete(:allow_equivalent) if options.has_key?(:allow_equivalent)
|
142
|
+
end
|
121
143
|
|
122
144
|
fk_options = options.dup
|
123
145
|
fk_options[:constraint_name] = options.delete(:constraint) if options.has_key?(:constraint)
|
124
|
-
fk_options[:index_name] = index_options[:name
|
146
|
+
fk_options[:index_name] = index_options&.[](:name)
|
125
147
|
|
126
148
|
fk = options[:foreign_key]&.to_s || "#{name}_id"
|
127
149
|
|
@@ -157,9 +179,9 @@ module DeclareSchema
|
|
157
179
|
if refl.options[:polymorphic]
|
158
180
|
foreign_type = options[:foreign_type] || "#{name}_type"
|
159
181
|
_declare_polymorphic_type_field(foreign_type, column_options)
|
160
|
-
index([foreign_type, fkey], **index_options) if index_options
|
182
|
+
index([foreign_type, fkey], **index_options) if index_options
|
161
183
|
else
|
162
|
-
index(fkey, **index_options) if index_options
|
184
|
+
index(fkey, **index_options) if index_options
|
163
185
|
constraint(fkey, **fk_options) if fk_options[:constraint_name] != false
|
164
186
|
end
|
165
187
|
end
|
@@ -5,12 +5,13 @@ require_relative 'base'
|
|
5
5
|
module DeclareSchema
|
6
6
|
module SchemaChange
|
7
7
|
class IndexAdd < Base
|
8
|
-
def initialize(table_name, column_names, name:, unique:, where: nil)
|
8
|
+
def initialize(table_name, column_names, name:, unique:, where: nil, limit: nil)
|
9
9
|
@table_name = table_name
|
10
10
|
@column_names = column_names
|
11
11
|
@name = name
|
12
12
|
@unique = unique
|
13
13
|
@where = where.presence
|
14
|
+
@limit = limit.presence
|
14
15
|
end
|
15
16
|
|
16
17
|
def up_command
|
@@ -19,6 +20,7 @@ module DeclareSchema
|
|
19
20
|
}
|
20
21
|
options[:unique] = true if @unique
|
21
22
|
options[:where] = @where if @where
|
23
|
+
options[:limit] = @limit if @limit
|
22
24
|
|
23
25
|
"add_index #{[@table_name.to_sym.inspect,
|
24
26
|
@column_names.map(&:to_sym).inspect,
|
@@ -330,7 +330,7 @@ module Generators
|
|
330
330
|
# TODO: TECH-5338: optimize that index doesn't need to be dropped on undo since entire table will be dropped
|
331
331
|
def create_indexes(model)
|
332
332
|
model.index_definitions.map do |i|
|
333
|
-
::DeclareSchema::SchemaChange::IndexAdd.new(model.table_name, i.columns,
|
333
|
+
::DeclareSchema::SchemaChange::IndexAdd.new(model.table_name, i.columns, **i.options)
|
334
334
|
end
|
335
335
|
end
|
336
336
|
|
@@ -108,8 +108,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
108
108
|
add_column :adverts, :published_at, :datetime, null: true
|
109
109
|
EOS
|
110
110
|
.and migrate_down(<<~EOS.strip)
|
111
|
-
|
112
|
-
|
111
|
+
remove_column :adverts, :published_at
|
112
|
+
remove_column :adverts, :body
|
113
113
|
EOS
|
114
114
|
)
|
115
115
|
|
@@ -141,8 +141,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
141
141
|
remove_column :adverts, :name
|
142
142
|
EOS
|
143
143
|
.and migrate_down(<<~EOS.strip)
|
144
|
-
|
145
|
-
|
144
|
+
add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
|
145
|
+
remove_column :adverts, :title
|
146
146
|
EOS
|
147
147
|
)
|
148
148
|
|
@@ -179,7 +179,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
179
179
|
change_column :adverts, :title, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
|
180
180
|
EOS
|
181
181
|
.and migrate_down(<<~EOS.strip)
|
182
|
-
|
182
|
+
change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
183
183
|
EOS
|
184
184
|
)
|
185
185
|
|
@@ -209,7 +209,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
209
209
|
change_column :adverts, :price, :integer, limit: 3, null: true
|
210
210
|
EOS
|
211
211
|
.and migrate_down(<<~EOS.strip)
|
212
|
-
|
212
|
+
change_column :adverts, :price, :integer, limit: 2, null: true
|
213
213
|
EOS
|
214
214
|
)
|
215
215
|
|
@@ -306,7 +306,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
306
306
|
change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
|
307
307
|
EOS
|
308
308
|
.and migrate_down(<<~EOS.strip)
|
309
|
-
|
309
|
+
change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
310
310
|
EOS
|
311
311
|
)
|
312
312
|
|
@@ -323,7 +323,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
323
323
|
change_column :adverts, :description, :text, limit: 4294967295, null: false#{charset_and_collation}
|
324
324
|
EOS
|
325
325
|
.and migrate_down(<<~EOS.strip)
|
326
|
-
|
326
|
+
change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
|
327
327
|
EOS
|
328
328
|
)
|
329
329
|
end
|
@@ -363,14 +363,14 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
363
363
|
#{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" if defined?(Mysql2)}
|
364
364
|
EOS
|
365
365
|
.and migrate_down(<<~EOS.strip)
|
366
|
-
|
367
|
-
|
368
|
-
|
366
|
+
#{"remove_foreign_key :adverts, name: :index_adverts_on_category_id" if defined?(Mysql2)}
|
367
|
+
remove_index :adverts, name: :index_adverts_on_category_id
|
368
|
+
remove_column :adverts, :category_id
|
369
369
|
EOS
|
370
370
|
)
|
371
371
|
|
372
372
|
Advert.field_specs.delete(:category_id)
|
373
|
-
Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
|
373
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
374
374
|
|
375
375
|
# If you specify a custom foreign key, the migration generator observes that:
|
376
376
|
|
@@ -411,7 +411,9 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
411
411
|
Advert.field_specs.delete(:category_id)
|
412
412
|
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
413
413
|
|
414
|
-
# You can specify the index name with :
|
414
|
+
# You can specify the index name with index: 'name' [deprecated]
|
415
|
+
|
416
|
+
expect(Kernel).to receive(:warn).with(/belongs_to index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
|
415
417
|
|
416
418
|
class Category < ActiveRecord::Base; end
|
417
419
|
class Advert < ActiveRecord::Base
|
@@ -431,6 +433,26 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
431
433
|
Advert.field_specs.delete(:category_id)
|
432
434
|
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
433
435
|
|
436
|
+
# You can specify the index name with index: { name: }'name', unique: true|false }
|
437
|
+
|
438
|
+
class Category < ActiveRecord::Base; end
|
439
|
+
class Advert < ActiveRecord::Base
|
440
|
+
declare_schema { }
|
441
|
+
belongs_to :category, index: { name: 'my_index', unique: false }
|
442
|
+
end
|
443
|
+
|
444
|
+
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
445
|
+
migrate_up(<<~EOS.strip)
|
446
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
447
|
+
add_index :adverts, [:category_id], name: :my_index
|
448
|
+
#{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
|
449
|
+
"add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
|
450
|
+
EOS
|
451
|
+
)
|
452
|
+
|
453
|
+
Advert.field_specs.delete(:category_id)
|
454
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
455
|
+
|
434
456
|
### Timestamps and Optimimistic Locking
|
435
457
|
|
436
458
|
# `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
|
@@ -452,11 +474,11 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
452
474
|
"add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
|
453
475
|
EOS
|
454
476
|
.and migrate_down(<<~EOS.strip)
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
477
|
+
#{"remove_foreign_key :adverts, name: :index_adverts_on_c_id\n" +
|
478
|
+
"remove_foreign_key :adverts, name: :index_adverts_on_category_id" if defined?(Mysql2)}
|
479
|
+
remove_column :adverts, :lock_version
|
480
|
+
remove_column :adverts, :updated_at
|
481
|
+
remove_column :adverts, :created_at
|
460
482
|
EOS
|
461
483
|
)
|
462
484
|
|
@@ -468,22 +490,29 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
468
490
|
|
469
491
|
# You can add an index to a field definition
|
470
492
|
|
493
|
+
expect(Kernel).to receive(:warn).with(/belongs_to index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
|
494
|
+
expect(Kernel).to receive(:warn).with(/belongs_to unique: true\|false is deprecated; use index: \{ unique: true\|false \} instead/i)
|
495
|
+
|
471
496
|
class Advert < ActiveRecord::Base
|
472
497
|
declare_schema do
|
473
498
|
string :title, index: true, limit: 250, null: true
|
474
499
|
end
|
500
|
+
belongs_to :category, index: 'my_index', unique: false
|
475
501
|
end
|
476
502
|
|
477
503
|
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
478
504
|
migrate_up(<<~EOS.strip)
|
479
505
|
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
506
|
+
add_column :adverts, :category_id, :integer, limit: 8, null: false
|
480
507
|
add_index :adverts, [:title], name: :index_adverts_on_title
|
508
|
+
add_index :adverts, [:category_id], name: :my_index
|
481
509
|
#{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
|
482
510
|
"add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
|
483
511
|
EOS
|
484
512
|
)
|
485
513
|
|
486
|
-
Advert.
|
514
|
+
Advert.field_specs.delete(:category_id)
|
515
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] || spec.fields == ["category_id"] }
|
487
516
|
|
488
517
|
# You can ask for a unique index
|
489
518
|
|
@@ -521,7 +550,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
521
550
|
EOS
|
522
551
|
)
|
523
552
|
|
524
|
-
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
553
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
525
554
|
|
526
555
|
# You can ask for an index outside of the fields block
|
527
556
|
|
@@ -540,16 +569,16 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
540
569
|
|
541
570
|
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
542
571
|
|
543
|
-
# The available options for the index function are
|
572
|
+
# The available options for the index function are :unique, :name, :where, and :length.
|
544
573
|
|
545
574
|
class Advert < ActiveRecord::Base
|
546
|
-
index :title, unique:
|
575
|
+
index :title, unique: false, name: 'my_index', length: 10
|
547
576
|
end
|
548
577
|
|
549
578
|
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
550
579
|
migrate_up(<<~EOS.strip)
|
551
580
|
add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
|
552
|
-
add_index :adverts, [:title], name: :my_index,
|
581
|
+
add_index :adverts, [:title], name: :my_index, length: 10
|
553
582
|
#{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
|
554
583
|
"add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
|
555
584
|
EOS
|
@@ -572,7 +601,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
572
601
|
EOS
|
573
602
|
)
|
574
603
|
|
575
|
-
Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
|
604
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title", "category_id"] }
|
576
605
|
|
577
606
|
# Finally, you can specify that the migration generator should completely ignore an
|
578
607
|
# index by passing its name to ignore_index in the model.
|
@@ -645,10 +674,10 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
645
674
|
remove_column :advertisements, :name
|
646
675
|
EOS
|
647
676
|
.and migrate_down(<<~EOS.strip)
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
677
|
+
add_column :advertisements, :name, :string, limit: 250, null: true#{charset_and_collation}
|
678
|
+
remove_column :advertisements, :body
|
679
|
+
remove_column :advertisements, :title
|
680
|
+
rename_table :advertisements, :adverts
|
652
681
|
EOS
|
653
682
|
)
|
654
683
|
|
@@ -665,9 +694,9 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
665
694
|
drop_table :adverts
|
666
695
|
EOS
|
667
696
|
.and migrate_down(<<~EOS.strip)
|
668
|
-
|
669
|
-
|
670
|
-
|
697
|
+
create_table "adverts"#{table_options}, force: :cascade do |t|
|
698
|
+
t.string "name", limit: 250#{charset_and_collation}
|
699
|
+
end
|
671
700
|
EOS
|
672
701
|
)
|
673
702
|
|
@@ -700,8 +729,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
700
729
|
add_index :adverts, [:type], name: :on_type
|
701
730
|
EOS
|
702
731
|
.and migrate_down(<<~EOS.strip)
|
703
|
-
|
704
|
-
|
732
|
+
remove_index :adverts, name: :on_type
|
733
|
+
remove_column :adverts, :type
|
705
734
|
EOS
|
706
735
|
)
|
707
736
|
end
|
@@ -709,7 +738,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
709
738
|
Advert.field_specs.delete(:type)
|
710
739
|
nuke_model_class(SuperFancyAdvert)
|
711
740
|
nuke_model_class(FancyAdvert)
|
712
|
-
Advert.index_definitions.delete_if { |spec| spec.fields==["type"] }
|
741
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["type"] }
|
713
742
|
|
714
743
|
## Coping with multiple changes
|
715
744
|
|
@@ -744,8 +773,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
744
773
|
change_column :adverts, :name, :string, limit: 250, null: true, default: "No Name"#{charset_and_collation}
|
745
774
|
EOS
|
746
775
|
.and migrate_down(<<~EOS.strip)
|
747
|
-
|
748
|
-
|
776
|
+
change_column :adverts, :name, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
|
777
|
+
rename_column :adverts, :name, :title
|
749
778
|
EOS
|
750
779
|
)
|
751
780
|
|
@@ -855,21 +884,21 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
855
884
|
it 'will genereate unique constraint names' do
|
856
885
|
expect(Generators::DeclareSchema::Migration::Migrator.run).to(
|
857
886
|
migrate_up(<<~EOS.strip)
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
887
|
+
create_table :categories, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
|
888
|
+
t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
|
889
|
+
end
|
890
|
+
create_table :advertisers, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
|
891
|
+
t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
|
892
|
+
t.integer :category_id, limit: 8, null: false
|
893
|
+
end
|
894
|
+
create_table :affiliates, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
|
895
|
+
t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
|
896
|
+
t.integer :category_id, limit: 8, null: false
|
897
|
+
end
|
898
|
+
add_index :advertisers, [:category_id], name: :index_advertisers_on_category_id
|
899
|
+
add_index :affiliates, [:category_id], name: :index_affiliates_on_category_id
|
900
|
+
add_foreign_key :advertisers, :categories, column: :category_id, name: :index_advertisers_on_category_id
|
901
|
+
add_foreign_key :affiliates, :categories, column: :category_id, name: :index_affiliates_on_category_id
|
873
902
|
EOS
|
874
903
|
)
|
875
904
|
migrate
|
@@ -36,26 +36,102 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
|
|
36
36
|
|
37
37
|
describe 'instance methods' do
|
38
38
|
let(:model) { model_class.new }
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
let(:fields) { ['last_name', 'first_name'] }
|
40
|
+
let(:options) { {} }
|
41
|
+
subject(:instance) { described_class.new(model_class, fields, **options) }
|
42
|
+
|
43
|
+
describe 'attr_readers' do
|
44
|
+
describe '#table' do
|
45
|
+
subject { instance.table }
|
46
|
+
|
47
|
+
it { is_expected.to eq(model_class.table_name) }
|
48
|
+
|
49
|
+
context 'with table_name option' do
|
50
|
+
let(:options) { { table_name: 'auth_users' } }
|
51
|
+
|
52
|
+
it { is_expected.to eq('auth_users') }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#fields' do
|
57
|
+
subject { instance.fields }
|
58
|
+
|
59
|
+
it { is_expected.to eq(fields) }
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#explicit_name' do
|
63
|
+
subject { instance.explicit_name }
|
64
|
+
|
65
|
+
it { is_expected.to eq(nil) }
|
66
|
+
|
67
|
+
context 'with name option' do
|
68
|
+
let(:options) { { name: 'index_auth_users_on_last_name_and_first_name' } }
|
69
|
+
|
70
|
+
it { is_expected.to eq('index_auth_users_on_last_name_and_first_name') }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#length' do
|
75
|
+
subject { instance.length }
|
76
|
+
let(:options) { { length: length } }
|
77
|
+
|
78
|
+
context 'with integer length' do
|
79
|
+
let(:length) { 2 }
|
80
|
+
|
81
|
+
it { is_expected.to eq(length) }
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with Hash length' do
|
85
|
+
let(:length) { { name: 2 } }
|
86
|
+
|
87
|
+
it { is_expected.to eq(length) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#options' do
|
92
|
+
subject { instance.options }
|
93
|
+
let(:options) { { name: 'my_index', unique: false, where: "(name like 'a%')", length: 10 } }
|
94
|
+
|
95
|
+
it { is_expected.to eq(options) }
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#with_name' do
|
99
|
+
subject { instance.with_name('new_name') }
|
100
|
+
|
101
|
+
it { is_expected.to be_kind_of(described_class) }
|
102
|
+
it { expect(instance.name).to eq('index_index_definition_test_models_on_last_name_and_first_name') }
|
103
|
+
it { expect(subject.name).to eq('new_name') }
|
104
|
+
end
|
47
105
|
end
|
106
|
+
end
|
48
107
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
[
|
58
|
-
|
108
|
+
describe 'class methods' do
|
109
|
+
let(:model) { model_class.new }
|
110
|
+
|
111
|
+
describe 'index_definitions' do
|
112
|
+
it do
|
113
|
+
expect(model_class.index_definitions.size).to eq(1)
|
114
|
+
|
115
|
+
expect(model_class.index_definitions[0].name).to eq('index_index_definition_test_models_on_name')
|
116
|
+
expect(model_class.index_definitions[0].fields).to eq(['name'])
|
117
|
+
expect(model_class.index_definitions[0].unique).to eq(false)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'has index_definitions_with_primary_key' do
|
122
|
+
it do
|
123
|
+
expect(model_class.index_definitions_with_primary_key).to be_kind_of(Array)
|
124
|
+
result = model_class.index_definitions_with_primary_key.sort_by(&:name)
|
125
|
+
expect(result.size).to eq(2)
|
126
|
+
|
127
|
+
expect(result[0].name).to eq('PRIMARY')
|
128
|
+
expect(result[0].fields).to eq(['id'])
|
129
|
+
expect(result[0].unique).to eq(true)
|
130
|
+
|
131
|
+
expect(result[1].name).to eq('index_index_definition_test_models_on_name')
|
132
|
+
expect(result[1].fields).to eq(['name'])
|
133
|
+
expect(result[1].unique).to eq(false)
|
134
|
+
end
|
59
135
|
end
|
60
136
|
end
|
61
137
|
|
@@ -86,12 +162,12 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
|
|
86
162
|
context 'with single-column PK' do
|
87
163
|
it 'returns the indexes for the model' do
|
88
164
|
expect(subject.size).to eq(2), subject.inspect
|
89
|
-
expect(
|
90
|
-
|
91
|
-
)
|
92
|
-
expect(
|
93
|
-
|
94
|
-
)
|
165
|
+
expect(subject[0].name).to eq('index_definition_test_models_on_name')
|
166
|
+
expect(subject[0].columns).to eq(['name'])
|
167
|
+
expect(subject[0].unique).to eq(true)
|
168
|
+
expect(subject[1].name).to eq('PRIMARY')
|
169
|
+
expect(subject[1].columns).to eq(['id'])
|
170
|
+
expect(subject[1].unique).to eq(true)
|
95
171
|
end
|
96
172
|
end
|
97
173
|
|
@@ -100,9 +176,9 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
|
|
100
176
|
|
101
177
|
it 'returns the indexes for the model' do
|
102
178
|
expect(subject.size).to eq(1), subject.inspect
|
103
|
-
expect(
|
104
|
-
|
105
|
-
)
|
179
|
+
expect(subject[0].name).to eq('PRIMARY')
|
180
|
+
expect(subject[0].columns).to eq(['fk1_id', 'fk2_id'])
|
181
|
+
expect(subject[0].unique).to eq(true)
|
106
182
|
end
|
107
183
|
end
|
108
184
|
end
|
@@ -30,7 +30,7 @@ RSpec.describe DeclareSchema::SchemaChange::IndexAdd do
|
|
30
30
|
end
|
31
31
|
|
32
32
|
context 'with where:' do
|
33
|
-
let(:where) { "'last_name like 'A%'"}
|
33
|
+
let(:where) { "'last_name like 'A%'" }
|
34
34
|
subject { described_class.new(table_name, column_names, name: name, unique: unique, where: where) }
|
35
35
|
|
36
36
|
it 'responds with command' do
|
@@ -45,6 +45,33 @@ RSpec.describe DeclareSchema::SchemaChange::IndexAdd do
|
|
45
45
|
expect(subject.up).to eq("add_index :#{table_name}, #{column_names.map(&:to_sym).inspect}, name: #{name.to_sym.inspect}, unique: true\n")
|
46
46
|
end
|
47
47
|
end
|
48
|
+
|
49
|
+
context 'with limit: nil' do
|
50
|
+
let(:limit) { nil }
|
51
|
+
subject { described_class.new(table_name, column_names, name: name, unique: unique, limit: limit) }
|
52
|
+
|
53
|
+
it 'responds with command' do
|
54
|
+
expect(subject.up).to eq("add_index :#{table_name}, #{column_names.map(&:to_sym).inspect}, name: #{name.to_sym.inspect}\n")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with limit: 2' do
|
59
|
+
let(:limit) { 2 }
|
60
|
+
subject { described_class.new(table_name, column_names, name: name, unique: unique, limit: limit) }
|
61
|
+
|
62
|
+
it 'responds with command' do
|
63
|
+
expect(subject.up).to eq("add_index :#{table_name}, #{column_names.map(&:to_sym).inspect}, name: #{name.to_sym.inspect}, limit: #{limit}\n")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with limit: hash' do
|
68
|
+
let(:limit) { { last_name: 10, first_name: 1 } }
|
69
|
+
subject { described_class.new(table_name, column_names, name: name, unique: unique, limit: limit) }
|
70
|
+
|
71
|
+
it 'responds with command' do
|
72
|
+
expect(subject.up).to eq("add_index :#{table_name}, #{column_names.map(&:to_sym).inspect}, name: #{name.to_sym.inspect}, limit: { last_name: 10, first_name: 1 }\n")
|
73
|
+
end
|
74
|
+
end
|
48
75
|
end
|
49
76
|
|
50
77
|
describe '#down' do
|