declare_schema 1.3.2 → 1.4.0.colin.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 474856e02a14224230a41cc4ef32234b8d20b69a647a30f810bce457c220261e
4
- data.tar.gz: 13994a873d83d5bce533b71f36c3057a0a0caf8c7559f809091a7dc6627f5f7c
3
+ metadata.gz: fe0a594fea50536d529c2d4e42280d9050726e1017524dbaff2b8a77e17c6b41
4
+ data.tar.gz: 89199ba5b58b936f93a7bfe0409c403b95da8bd771b425b2c61a6afd8a951f2a
5
5
  SHA512:
6
- metadata.gz: 7b298b54b826e8bb464c544ab2015ddca0a761cda03f03724598158617b89cef0b0f19521a103898e92157cad11d32e9feb71c1293a36bbc2c3a666c123d444f
7
- data.tar.gz: 5e0c12a370ed83eeb31f398e124597a00dd7b5872d08b8c47e1eb2fb5842c6b295b251d2b58ff26619af8c92ca0342ac8bf585652c9822409dfab40798d0f086
6
+ metadata.gz: ff4858ac26953e22a9dbf37ce5b2391295d05ec5a8401361434987e6e90c7fc01543f2bbb1cebaf91a0ce16dadb8ac5c8600d01130c81f361c3728bcd7c23f9b
7
+ data.tar.gz: 1fabf741327417f5beb76ba694de17e0176db5f63e95922a03d195eb4dcff60d71434cd6aeb4e8ff8212bc0b1fc7cc5a8cb400307efeb87a83593887621d366b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ 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.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
+
7
13
  ## [1.3.2] - 2024-01-12
8
14
  ### Fixed
9
15
  - Fix bug in migrator when table option definitions differ
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (1.3.2)
4
+ declare_schema (1.4.0.colin.1)
5
5
  rails (>= 5.0)
6
6
 
7
7
  GEM
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: true, unique: true
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 the global level to customize default declaration for the following values:
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
- The default value for generate foreign keys can be set using the `DeclareSchema.default_generate_foreign_keys=` method.
176
- This value defaults to `true` and can only be set at the global level.
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 set
179
- the default `generate foreign keys` value to `false`:
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
- The default value for generate indexing can be set using the `DeclareSchema.default_generate_indexing=` method.
190
- This value defaults to `true` and can only be set at the global level.
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
- set the default `generate indexing` value to `false`:
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 the global level
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
- `DeclareSchema.db_migrate_command = "bundle exec rails db:migrate_immediate"`
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 accounts for it.
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` probably should not alter it. Accordingly,
242
- `declare_schema` can be configured to ignore tables.
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
- attr_reader :table, :fields, :explicit_name, :name, :unique, :where
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[:where])
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, name, unique, where].map(&:to_s)
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, unique].map(&:to_s)
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, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
139
+ self.class.new(@model, @fields, **{ **options, name: new_name })
127
140
  end
128
141
 
129
142
  alias eql? ==
@@ -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
- index_options = {}
118
- index_options[:name] = options.delete(:index) if options.has_key?(:index)
119
- index_options[:unique] = options.delete(:unique) if options.has_key?(:unique)
120
- index_options[:allow_equivalent] = options.delete(:allow_equivalent) if options.has_key?(:allow_equivalent)
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[:name] != false
182
+ index([foreign_type, fkey], **index_options) if index_options
161
183
  else
162
- index(fkey, **index_options) if index_options[:name] != false
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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "1.3.2"
4
+ VERSION = "1.4.0.colin.1"
5
5
  end
@@ -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, unique: i.unique, where: i.where, name: i.name)
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
- remove_column :adverts, :published_at
112
- remove_column :adverts, :body
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
- add_column :adverts, :name, :string, limit: 250, null: true#{charset_and_collation}
145
- remove_column :adverts, :title
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
- change_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
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
- change_column :adverts, :price, :integer, limit: 2, null: true
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
- change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
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
- change_column :adverts, :description, :text#{', limit: 255' if defined?(Mysql2)}, null: true#{charset_and_collation}
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
- #{"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
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 :index
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
- #{"remove_foreign_key :adverts, name: :index_adverts_on_c_id\n" +
456
- "remove_foreign_key :adverts, name: :index_adverts_on_category_id" if defined?(Mysql2)}
457
- remove_column :adverts, :lock_version
458
- remove_column :adverts, :updated_at
459
- remove_column :adverts, :created_at
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.index_definitions.delete_if { |spec| spec.fields==["title"] }
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 `:unique` and `:name`
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: true, name: 'my_index'
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, unique: true
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
- add_column :advertisements, :name, :string, limit: 250, null: true#{charset_and_collation}
649
- remove_column :advertisements, :body
650
- remove_column :advertisements, :title
651
- rename_table :advertisements, :adverts
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
- create_table "adverts"#{table_options}, force: :cascade do |t|
669
- t.string "name", limit: 250#{charset_and_collation}
670
- end
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
- remove_index :adverts, name: :on_type
704
- remove_column :adverts, :type
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
- change_column :adverts, :name, :string, limit: 250, null: true, default: "Untitled"#{charset_and_collation}
748
- rename_column :adverts, :name, :title
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
- create_table :categories, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
859
- t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
860
- end
861
- create_table :advertisers, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
862
- t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
863
- t.integer :category_id, limit: 8, null: false
864
- end
865
- create_table :affiliates, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
866
- t.string :name, limit: 250, null: true, charset: "utf8mb4", collation: "utf8mb4_bin"
867
- t.integer :category_id, limit: 8, null: false
868
- end
869
- add_index :advertisers, [:category_id], name: :index_advertisers_on_category_id
870
- add_index :affiliates, [:category_id], name: :index_affiliates_on_category_id
871
- add_foreign_key :advertisers, :categories, column: :category_id, name: :index_advertisers_on_category_id
872
- add_foreign_key :affiliates, :categories, column: :category_id, name: :index_affiliates_on_category_id
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
- subject { declared_class.new(model_class) }
40
-
41
- it 'has index_definitions' do
42
- expect(model_class.index_definitions).to be_kind_of(Array)
43
- expect(model_class.index_definitions.map(&:name)).to eq(['index_index_definition_test_models_on_name'])
44
- expect([:name, :fields, :unique].map { |attr| model_class.index_definitions[0].send(attr)}).to eq(
45
- ['index_index_definition_test_models_on_name', ['name'], false]
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
- it 'has index_definitions_with_primary_key' do
50
- expect(model_class.index_definitions_with_primary_key).to be_kind_of(Array)
51
- result = model_class.index_definitions_with_primary_key.sort_by(&:name)
52
- expect(result.map(&:name)).to eq(['PRIMARY', 'index_index_definition_test_models_on_name'])
53
- expect([:name, :fields, :unique].map { |attr| result[0].send(attr)}).to eq(
54
- ['PRIMARY', ['id'], true]
55
- )
56
- expect([:name, :fields, :unique].map { |attr| result[1].send(attr)}).to eq(
57
- ['index_index_definition_test_models_on_name', ['name'], false]
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([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
90
- ['index_definition_test_models_on_name', ['name'], true]
91
- )
92
- expect([:name, :columns, :unique].map { |attr| subject[1].send(attr) }).to eq(
93
- ['PRIMARY', ['id'], true]
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([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
104
- ['PRIMARY', ['fk1_id', 'fk2_id'], true]
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: declare_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0.colin.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca Development adapted from hobo_fields by Tom Locke