declare_schema 1.3.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d142143cff509bcd40c65523866ce41b01ff9e5d5eb33914cbe22446f2915d18
4
- data.tar.gz: 01c7ab3b0a606c4b21f9b3b4f67cceff09d6b20c699ec291036bd840ca084d86
3
+ metadata.gz: fe0a594fea50536d529c2d4e42280d9050726e1017524dbaff2b8a77e17c6b41
4
+ data.tar.gz: 89199ba5b58b936f93a7bfe0409c403b95da8bd771b425b2c61a6afd8a951f2a
5
5
  SHA512:
6
- metadata.gz: 0264bf26889a2149ae448d8b874f74c32d5fc8a5877a02466e89b62dd5000e7411c4e3dcf2ea6be8534f259a5ae9ebe0bab5f514fc18e4d8629ecd203ebce68a
7
- data.tar.gz: b454598ee79b3c3bf6677c3c9e1938931d882755861ad87c546e30f5d2e4d284356bf7fc41936a1e164193786af7ccc9dc2442d61c3af56c37ae339d8166a32b
6
+ metadata.gz: ff4858ac26953e22a9dbf37ce5b2391295d05ec5a8401361434987e6e90c7fc01543f2bbb1cebaf91a0ce16dadb8ac5c8600d01130c81f361c3728bcd7c23f9b
7
+ data.tar.gz: 1fabf741327417f5beb76ba694de17e0176db5f63e95922a03d195eb4dcff60d71434cd6aeb4e8ff8212bc0b1fc7cc5a8cb400307efeb87a83593887621d366b
data/CHANGELOG.md CHANGED
@@ -4,24 +4,11 @@ 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.3.6] - 2024-01-22
8
- ### Fixed
9
- - Add missing commits around connection: and Array check for composite declared primary key.
10
-
11
- ## [1.3.5] - 2024-01-22
12
- ### Fixed
13
- - Make `default_charset=` and `default_collation=` lazy so they don't use the database connection to check the
14
- MySQL version. Instead, that is checked the first time `default_charset` or `default_collation` is called.
15
-
16
- ## [1.3.4] - 2024-01-18
17
- ### Fixed
18
- - Add test for migrating `has_and_belongs_to_many` associations and fix them to properly declare their
19
- 2 foreign keys as the primary key of the join table, rather than just a unique index.
20
-
21
- ## [1.3.3] - 2024-01-17
22
- ### Fixed
23
- - Fix a MySQL 8 bug where MySQL 8+ renames charset 'utf8' to 'utf8mb3' and collation 'utf8_general_ci' to
24
- 'utf8mb3_unicode_ci'.
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 }.
25
12
 
26
13
  ## [1.3.2] - 2024-01-12
27
14
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (1.3.6)
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,14 +253,9 @@ 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
  ```
216
- Note: MySQL 8+ aliases charset 'utf8' to 'utf8mb3', and 'utf8_general_ci' to 'utf8mb3_unicode_ci',
217
- so when running on MySQL 8+, those aliases will be applied by `DeclareSchema`.
218
-
219
259
  #### db:migrate Command
220
260
  `declare_schema` can run the migration once it is generated, if the `--migrate` option is passed.
221
261
  If not, it will display the command to run later. By default this command is
@@ -224,7 +264,7 @@ bundle exec rails db:migrate
224
264
  ```
225
265
  If your repo has a different command to run for migrations, you can configure it like this:
226
266
  ```ruby
227
- `DeclareSchema.db_migrate_command = "bundle exec rails db:migrate_immediate"`
267
+ DeclareSchema.db_migrate_command = "bundle exec rails db:migrate_immediate"
228
268
  ```
229
269
 
230
270
  ## The `belongs_to` Association
@@ -233,24 +273,16 @@ association is outside of the `declare_schema do` block, so `declare_schema` int
233
273
  infer the foreign key column.
234
274
 
235
275
  By default, `declare_schema` creates an index for `belongs_to` relations. If this default index is not desired,
236
- you can use `index: false` in the `belongs_to` expression. This may be the case if for example a different index
237
- 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.
238
278
 
239
279
  ## The `has_and_belongs_to_many` Association
240
280
  Like the `belongs_to` association, `has_and_belongs_to_many` is outside of the `declare_schema ` block. `declare_schema` similarly
241
281
  infers foreign keys (and the intersection table).
242
282
 
243
283
  ## Ignored Tables
244
- If a table's schema or metadata are managed elsewhere, `declare_schema` probably should not alter it. Accordingly,
245
- `declare_schema` can be configured to ignore tables.
246
-
247
- `declare_schema` by default ignores these tables:
248
- - The ActiveRecord `schema_info` table
249
- - The ActiveRecord schema migrations table (generally named `schema_migrations`)
250
- - The ActiveRecord internal metadata table (generally named `ar_internal_metadata`)
251
- - If defined/configured, the CGI ActiveRecordStore session table
252
-
253
- 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`.
254
286
  For example:
255
287
 
256
288
  ```ruby
@@ -261,6 +293,12 @@ For example:
261
293
  ]
262
294
  ```
263
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
+
264
302
  ## Maximum Length of Index and Constraint Names
265
303
 
266
304
  MySQL limits the length of index and constraint names to 64 characters.
@@ -282,7 +320,10 @@ But later, Unicode was extended beyond U+FFFF to make room for emojis, and with
282
320
  UTF-8 require 1-4 bytes (`mb4` or "multi-byte 4"). With this addition, there has
283
321
  come a need to dynamically define the character set and collation for individual
284
322
  tables and columns in the database. With `declare_schema` this can be configured
285
- 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.
286
327
 
287
328
  ### Table Configuration
288
329
  In order to configure a table's default character set and collation, the `charset` and
@@ -107,9 +107,7 @@ module DeclareSchema
107
107
  if @type.in?([:text, :string])
108
108
  if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
109
109
  @options[:charset] ||= model._table_options&.[](:charset) || ::DeclareSchema.default_charset
110
- @options[:charset] = DeclareSchema.normalize_charset(@options[:charset])
111
110
  @options[:collation] ||= model._table_options&.[](:collation) || ::DeclareSchema.default_collation
112
- @options[:collation] = DeclareSchema.normalize_collation(@options[:collation])
113
111
  else
114
112
  @options.delete(:charset)
115
113
  @options.delete(:collation)
@@ -7,53 +7,46 @@ module DeclareSchema
7
7
  class ForeignKeyDefinition
8
8
  include Comparable
9
9
 
10
- attr_reader :foreign_key_column, :constraint_name, :child_table_name, :parent_table_name, :dependent
11
-
12
- # Caller needs to pass either constraint_name or child_table. The child_table is remembered, but it is not part of the key;
13
- # it is just used to compute the default constraint_name if no constraint_name is given.
14
- def initialize(foreign_key_column, constraint_name: nil, child_table: nil, parent_table: nil, class_name: nil, dependent: nil)
15
- @foreign_key_column = foreign_key_column&.to_s or raise ArgumentError "foreign key must not be empty: #{foreign_key_column.inspect}"
16
- @constraint_name = constraint_name&.to_s.presence || ::DeclareSchema::Model::IndexDefinition.default_index_name(child_table, [@foreign_key_column])
17
- @child_table_name = child_table&.to_s or raise ArgumentError, "child_table must not be nil"
18
- @parent_table_name = parent_table&.to_s || infer_parent_table_name_from_class(class_name) || infer_parent_table_name_from_foreign_key_column(@foreign_key_column)
19
- dependent.in?([nil, :delete]) or raise ArgumentError, "dependent: must be nil or :delete"
20
- @dependent = dependent
10
+ attr_reader :constraint_name, :model, :foreign_key, :foreign_key_name, :parent_table_name, :child_table_name, :options, :on_delete_cascade
11
+
12
+
13
+ def initialize(model, foreign_key, **options)
14
+ @model = model
15
+ @foreign_key = foreign_key.to_s.presence or raise ArgumentError "Foreign key must not be empty: #{foreign_key.inspect}"
16
+ @options = options
17
+
18
+ @child_table_name = model.table_name # unless a table rename, which would happen when a class is renamed??
19
+ @parent_table_name = options[:parent_table]&.to_s
20
+ @foreign_key_name = options[:foreign_key]&.to_s || @foreign_key
21
+
22
+ @constraint_name = options[:constraint_name]&.to_s.presence ||
23
+ model.connection.index_name(model.table_name, column: @foreign_key_name)
24
+ @on_delete_cascade = options[:dependent] == :delete
21
25
  end
22
26
 
23
27
  class << self
24
- def for_table(table_name, connection, dependent: nil)
25
- show_create_table = connection.select_rows("show create table #{connection.quote_table_name(table_name)}").first.last
28
+ def for_model(model, old_table_name)
29
+ show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
26
30
  constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
27
31
 
28
32
  constraints.map do |fkc|
29
- constraint_name, foreign_key_column, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
30
- dependent_value = :delete if dependent || fkc['ON DELETE CASCADE']
31
-
32
- new(foreign_key_column, constraint_name: constraint_name, child_table: table_name, parent_table: parent_table, dependent: dependent_value)
33
+ name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
34
+ options = {
35
+ constraint_name: name,
36
+ parent_table: parent_table,
37
+ foreign_key: foreign_key
38
+ }
39
+ options[:dependent] = :delete if fkc['ON DELETE CASCADE'] || model.is_a?(DeclareSchema::Model::HabtmModelShim)
40
+
41
+ new(model, foreign_key, **options)
33
42
  end
34
43
  end
35
44
  end
36
45
 
37
- def key
38
- @key ||= [@parent_table_name, @foreign_key_column, @dependent].freeze
39
- end
40
-
41
- def <=>(rhs)
42
- key <=> rhs.key
43
- end
44
-
45
- alias eql? ==
46
-
47
- def equivalent?(rhs)
48
- self == rhs
49
- end
50
-
51
- private
52
-
53
46
  # returns the parent class as a Class object
54
- # or nil if no @class_name option given
55
- def parent_class(class_name)
56
- if class_name
47
+ # or nil if no :class_name option given
48
+ def parent_class
49
+ if (class_name = options[:class_name])
57
50
  if class_name.is_a?(Class)
58
51
  class_name
59
52
  else
@@ -62,12 +55,22 @@ module DeclareSchema
62
55
  end
63
56
  end
64
57
 
65
- def infer_parent_table_name_from_class(class_name)
66
- parent_class(class_name)&.try(:table_name)
58
+ def parent_table_name
59
+ @parent_table_name ||=
60
+ parent_class&.try(:table_name) ||
61
+ foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
62
+ end
63
+
64
+ def <=>(rhs)
65
+ key <=> rhs.send(:key)
67
66
  end
68
67
 
69
- def infer_parent_table_name_from_foreign_key_column(foreign_key_column)
70
- foreign_key_column.sub(/_id\z/, '').camelize.constantize.table_name
68
+ alias eql? ==
69
+
70
+ private
71
+
72
+ def key
73
+ @key ||= [@child_table_name, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
71
74
  end
72
75
 
73
76
  def hash
@@ -4,24 +4,28 @@ module DeclareSchema
4
4
  module Model
5
5
  class HabtmModelShim
6
6
  class << self
7
- def from_reflection(reflection)
8
- new(reflection.join_table,
9
- [reflection.foreign_key, reflection.association_foreign_key],
10
- [reflection.active_record.table_name, reflection.klass.table_name],
11
- connection: reflection.active_record.connection)
7
+ def from_reflection(refl)
8
+ join_table = refl.join_table
9
+ foreign_keys_and_classes = [
10
+ [refl.foreign_key.to_s, refl.active_record],
11
+ [refl.association_foreign_key.to_s, refl.class_name.constantize]
12
+ ].sort { |a, b| a.first <=> b.first }
13
+ foreign_keys = foreign_keys_and_classes.map(&:first)
14
+ foreign_key_classes = foreign_keys_and_classes.map(&:last)
15
+ # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
16
+ # figure that anybody who sets THAT up can deal with their own migrations...
17
+ connection = refl.active_record.connection
18
+
19
+ new(join_table, foreign_keys, foreign_key_classes, connection)
12
20
  end
13
21
  end
14
22
 
15
- attr_reader :join_table, :foreign_keys, :parent_table_names, :connection
23
+ attr_reader :join_table, :foreign_keys, :foreign_key_classes, :connection
16
24
 
17
- def initialize(join_table, foreign_keys, parent_table_names, connection:)
18
- foreign_keys.is_a?(Array) && foreign_keys.size == 2 or
19
- raise ArgumentError, "foreign_keys must be <Array[2]>; got #{foreign_keys.inspect}"
20
- parent_table_names.is_a?(Array) && parent_table_names.size == 2 or
21
- raise ArgumentError, "parent_table_names must be <Array[2]>; got #{parent_table_names.inspect}"
25
+ def initialize(join_table, foreign_keys, foreign_key_classes, connection)
22
26
  @join_table = join_table
23
- @foreign_keys = foreign_keys.sort # Rails requires these be in alphabetical order
24
- @parent_table_names = @foreign_keys == foreign_keys ? parent_table_names : parent_table_names.reverse # match the above sort
27
+ @foreign_keys = foreign_keys
28
+ @foreign_key_classes = foreign_key_classes
25
29
  @connection = connection
26
30
  end
27
31
 
@@ -34,8 +38,8 @@ module DeclareSchema
34
38
  end
35
39
 
36
40
  def field_specs
37
- foreign_keys.each_with_index.each_with_object({}) do |(foreign_key, i), result|
38
- result[foreign_key] = ::DeclareSchema::Model::FieldSpec.new(self, foreign_key, :bigint, position: i, null: false)
41
+ foreign_keys.each_with_index.each_with_object({}) do |(v, position), result|
42
+ result[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :bigint, position: position, null: false)
39
43
  end
40
44
  end
41
45
 
@@ -44,30 +48,26 @@ module DeclareSchema
44
48
  end
45
49
 
46
50
  def _declared_primary_key
47
- foreign_keys
48
- end
49
-
50
- def index_definitions
51
- [
52
- IndexDefinition.new(foreign_keys.last, table_name: table_name, unique: false) # index for queries where we only have the last foreign key
53
- ]
51
+ false # no single-column primary key declared
54
52
  end
55
53
 
56
54
  def index_definitions_with_primary_key
57
55
  [
58
- *index_definitions,
59
- IndexDefinition.new(foreign_keys, name: Model::IndexDefinition::PRIMARY_KEY_NAME, unique: true) # creates a primary composite key on both foreign keys
56
+ IndexDefinition.new(self, foreign_keys, unique: true, name: Model::IndexDefinition::PRIMARY_KEY_NAME), # creates a primary composite key on both foreign keys
57
+ IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
60
58
  ]
61
59
  end
62
60
 
61
+ alias_method :index_definitions, :index_definitions_with_primary_key
62
+
63
63
  def ignore_indexes
64
64
  []
65
65
  end
66
66
 
67
67
  def constraint_specs
68
68
  [
69
- ForeignKeyDefinition.new(foreign_keys.first, child_table: @join_table, parent_table: parent_table_names.first, constraint_name: "#{join_table}_FK1", dependent: :delete),
70
- ForeignKeyDefinition.new(foreign_keys.last, child_table: @join_table, parent_table: parent_table_names.last, constraint_name: "#{join_table}_FK2", dependent: :delete)
69
+ ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
70
+ ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
71
71
  ]
72
72
  end
73
73
  end
@@ -7,63 +7,65 @@ module DeclareSchema
7
7
  class IndexDefinition
8
8
  include Comparable
9
9
 
10
- attr_reader :columns, :explicit_name, :name, :unique, :where
11
- alias fields columns # TODO: change callers to use columns. -Colin
10
+ # TODO: replace `fields` with `columns` and remove alias. -Colin
11
+ OPTIONS = [:name, :unique, :where, :length].freeze
12
+ attr_reader :table, :fields, :explicit_name, *OPTIONS
13
+ alias columns fields
12
14
 
13
15
  class IndexNameTooLongError < RuntimeError; end
14
16
 
15
17
  PRIMARY_KEY_NAME = "PRIMARY"
16
18
 
17
- # Caller needs to pass either name or table_name. The table_name is not remembered; it is just used to compute the
18
- # default name if no name is given.
19
- def initialize(columns, name: nil, table_name: nil, allow_equivalent: false, unique: false, where: nil)
20
- @name = name || self.class.default_index_name(table_name, columns)
21
- @columns = Array.wrap(columns).map(&:to_s)
22
- @explicit_name = @name unless allow_equivalent
23
- unique.in?([false, true]) or raise ArgumentError, "unique must be true or false: got #{unique.inspect}"
24
- if @name == PRIMARY_KEY_NAME
25
- unique or raise ArgumentError, "primary key index must be unique"
26
- end
27
- @unique = unique
19
+ def initialize(model, fields, **options)
20
+ @model = model
21
+ @table = options.delete(:table_name) || model.table_name
22
+ @fields = Array.wrap(fields).map(&:to_s)
23
+ @explicit_name = options[:name] unless options.delete(:allow_equivalent)
24
+ @name = options.delete(:name) || self.class.default_index_name(@table, @fields)
25
+ @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
26
+ @length = options.delete(:length)
28
27
 
29
28
  if DeclareSchema.max_index_and_constraint_name_length && @name.length > DeclareSchema.max_index_and_constraint_name_length
30
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."
31
30
  end
32
31
 
33
- if where
32
+ if (where = options.delete(:where))
34
33
  @where = where.start_with?('(') ? where : "(#{where})"
35
34
  end
35
+
36
+ options.any? and warn("ignoring unrecognized option(s): #{options.inspect} for model #{model}")
36
37
  end
37
38
 
38
39
  class << self
39
40
  # extract IndexSpecs from an existing table
40
41
  # includes the PRIMARY KEY index
41
- def for_table(table_name, ignore_indexes, connection)
42
- primary_key_columns = Array(connection.primary_key(table_name))
43
- primary_key_columns.present? or raise "could not find primary key for table #{table_name} in #{connection.columns(table_name).inspect}"
42
+ def for_model(model, old_table_name = nil)
43
+ t = old_table_name || model.table_name
44
44
 
45
- primary_key_found = false
46
- index_definitions = connection.indexes(table_name).map do |index|
47
- next if ignore_indexes.include?(index.name)
45
+ primary_key_columns = Array(model.connection.primary_key(t)).presence
46
+ primary_key_columns or raise "could not find primary key for table #{t} in #{model.connection.columns(t).inspect}"
48
47
 
49
- if index.name == PRIMARY_KEY_NAME
50
- index.columns == primary_key_columns && index.unique or
51
- raise "primary key on #{table_name} was not unique on #{primary_key_columns} (was unique=#{index.unique} on #{index.columns})"
48
+ primary_key_found = false
49
+ index_definitions = model.connection.indexes(t).map do |i|
50
+ model.ignore_indexes.include?(i.name) and next
51
+ if i.name == PRIMARY_KEY_NAME
52
+ i.columns == primary_key_columns && i.unique or
53
+ raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
52
54
  primary_key_found = true
53
55
  end
54
- new(index.columns, name: index.name, unique: index.unique, where: index.where)
56
+ new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
55
57
  end.compact
56
58
 
57
59
  if !primary_key_found
58
- index_definitions << new(primary_key_columns, name: PRIMARY_KEY_NAME, unique: true)
60
+ index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
59
61
  end
60
62
  index_definitions
61
63
  end
62
64
 
63
- def default_index_name(table_name, columns)
65
+ def default_index_name(table, fields)
64
66
  index_name = nil
65
67
  [:long_index_name, :short_index_name].find do |method_name|
66
- index_name = send(method_name, table_name, columns)
68
+ index_name = send(method_name, table, fields)
67
69
  if DeclareSchema.max_index_and_constraint_name_length.nil? || index_name.length <= DeclareSchema.max_index_and_constraint_name_length
68
70
  break index_name
69
71
  end
@@ -104,12 +106,21 @@ module DeclareSchema
104
106
  name == PRIMARY_KEY_NAME
105
107
  end
106
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.
107
117
  def to_key
108
- @to_key ||= [name, *settings].freeze
118
+ @key ||= [table, fields, options].freeze
109
119
  end
110
120
 
121
+ # The index settings for this object. Used for equivalence checking. Does not include the name.
111
122
  def settings
112
- @settings ||= [columns, unique, where].freeze
123
+ @settings ||= [table, fields, options.except(:name)].freeze
113
124
  end
114
125
 
115
126
  def hash
@@ -125,7 +136,7 @@ module DeclareSchema
125
136
  end
126
137
 
127
138
  def with_name(new_name)
128
- self.class.new(@columns, name: new_name, unique: @unique, allow_equivalent: @explicit_name.nil?, where: @where)
139
+ self.class.new(@model, @fields, **{ **options, name: new_name })
129
140
  end
130
141
 
131
142
  alias eql? ==
@@ -50,17 +50,7 @@ module DeclareSchema
50
50
 
51
51
  def initialize(table_name, **table_options)
52
52
  @table_name = table_name
53
- @table_options = table_options.each_with_object({}) do |(k, v),result|
54
- result[k] =
55
- case k
56
- when :charset
57
- DeclareSchema.normalize_charset(v)
58
- when :collation
59
- DeclareSchema.normalize_collation(v)
60
- else
61
- v
62
- end
63
- end
53
+ @table_options = table_options
64
54
  end
65
55
 
66
56
  def to_key