declare_schema 1.3.6 → 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: 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