declare_schema 0.3.1 → 0.4.0

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: 8b82febb09d3ef72dd3e219c6900abd790e6401e9b15c9ca05c8cd9fc20c5bf4
4
- data.tar.gz: 3f2520292af5845df795dba3f26ee62385d1166da51ce5c6a980d1fdc52460a3
3
+ metadata.gz: 1e5c665ec203dd84e445290bac1de190491560ddc409fe2bd3f5ba4831e065c5
4
+ data.tar.gz: 244148b892d2de22a692c4db50b136995f97d8da38573e87a443303fdee5be7a
5
5
  SHA512:
6
- metadata.gz: bb726c72430d5b44d94239c3d00abfa42a1c4e2a9c23ad316bd66ec6567857a81c6f7766136bf3d4d78d1df7a43edec2da7e0162cabd60080892b29a2f6b69ce
7
- data.tar.gz: 4f39c4534895f33f5d30181ef18b09c2fd9eb535e9da14b0ce502c9117cf28e58404e4c6b2254ffb322a3faea99f0433de6450b9fca96a0585bc87d25047c7b8
6
+ metadata.gz: 44994fd571b97768ffec2c9bbce779b8beef222ba50213a425a3021c66f1dc4e4fd7a50adc95cf3d1a1116e67eaf5c47b11e635408856c726118a22c18188b98
7
+ data.tar.gz: 7d3eb3be095a1dce65b6b39b35974a6c96ee940fec31dba3fddbfb528be8be3b42906ec81c6075cde5d8fa590df4afc8ca12887a956c772fc34c084791577526
@@ -4,6 +4,19 @@ 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
+ ## [0.4.0] - 2020-11-20
8
+ ### Added
9
+ - Fields may be declared with `serialize: true` or `serialize: <serializeable-class>`, where `<serializeable-class>`
10
+ may be `Array` (`Array` stored as YAML) or `Hash` (`Hash` stored as YAML), (`Array` or `Hash` or any scalar value stored as JSON)
11
+ or any custom serializable class.
12
+ This invokes `ActiveSupport`'s `serialize` macro for that field, passing the serializable class, if given.
13
+
14
+ Note: when `serialize:` is used, any `default:` should be given in a matching Ruby type--for example, `[]` or `{}` or `{ 'currency' => 'USD' }`--in
15
+ which case the serializeable class will be used to determine the serialized default value and that will be set as the SQL default.
16
+
17
+ ### Fixed
18
+ - Sqlite now correctly infers the PRIMARY KEY so it won't attempt to add that index again.
19
+
7
20
  ## [0.3.1] - 2020-11-13
8
21
  ### Fixed
9
22
  - When passing `belongs_to` to Rails, suppress the `optional:` option in Rails 4, since that option was added in Rails 5.
@@ -47,6 +60,7 @@ using the appropriate Rails configuration attributes.
47
60
  ### Added
48
61
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
49
62
 
63
+ [0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
50
64
  [0.3.1]: https://github.com/Invoca/declare_schema/compare/v0.3.0...v0.3.1
51
65
  [0.3.0]: https://github.com/Invoca/declare_schema/compare/v0.2.0...v0.3.0
52
66
  [0.2.0]: https://github.com/Invoca/declare_schema/compare/v0.1.3...v0.2.0
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.3.1)
4
+ declare_schema (0.4.0)
5
5
  rails (>= 4.2)
6
6
 
7
7
  GEM
@@ -39,6 +39,7 @@ require 'declare_schema/extensions/active_record/fields_declaration'
39
39
  require 'declare_schema/field_declaration_dsl'
40
40
  require 'declare_schema/model'
41
41
  require 'declare_schema/model/field_spec'
42
- require 'declare_schema/model/index_spec'
42
+ require 'declare_schema/model/index_definition'
43
+ require 'declare_schema/model/foreign_key_definition'
43
44
 
44
45
  require 'declare_schema/railtie' if defined?(Rails)
@@ -25,8 +25,8 @@ module DeclareSchema
25
25
  # and speeds things up a little.
26
26
  inheriting_cattr_reader field_specs: HashWithIndifferentAccess.new
27
27
 
28
- # index_specs holds IndexSpec objects for all the declared indexes.
29
- inheriting_cattr_reader index_specs: []
28
+ # index_definitions holds IndexDefinition objects for all the declared indexes.
29
+ inheriting_cattr_reader index_definitions: []
30
30
  inheriting_cattr_reader ignore_indexes: []
31
31
  inheriting_cattr_reader constraint_specs: []
32
32
 
@@ -51,19 +51,19 @@ module DeclareSchema
51
51
  def index(fields, options = {})
52
52
  # don't double-index fields
53
53
  index_fields_s = Array.wrap(fields).map(&:to_s)
54
- unless index_specs.any? { |index_spec| index_spec.fields == index_fields_s }
55
- index_specs << ::DeclareSchema::Model::IndexSpec.new(self, fields, options)
54
+ unless index_definitions.any? { |index_spec| index_spec.fields == index_fields_s }
55
+ index_definitions << ::DeclareSchema::Model::IndexDefinition.new(self, fields, options)
56
56
  end
57
57
  end
58
58
 
59
59
  def primary_key_index(*fields)
60
- index(fields.flatten, unique: true, name: "PRIMARY_KEY")
60
+ index(fields.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
61
61
  end
62
62
 
63
63
  def constraint(fkey, options = {})
64
64
  fkey_s = fkey.to_s
65
65
  unless constraint_specs.any? { |constraint_spec| constraint_spec.foreign_key == fkey_s }
66
- constraint_specs << DeclareSchema::Model::ForeignKeySpec.new(self, fkey, options)
66
+ constraint_specs << DeclareSchema::Model::ForeignKeyDefinition.new(self, fkey, options)
67
67
  end
68
68
  end
69
69
 
@@ -79,19 +79,20 @@ module DeclareSchema
79
79
  # declarations.
80
80
  def declare_field(name, type, *args)
81
81
  options = args.extract_options!
82
- field_added(name, type, args, options) if respond_to?(:field_added)
83
- add_formatting_for_field(name, type, args)
82
+ try(:field_added, name, type, args, options)
83
+ add_serialize_for_field(name, type, options)
84
+ add_formatting_for_field(name, type)
84
85
  add_validations_for_field(name, type, args, options)
85
86
  add_index_for_field(name, args, options)
86
87
  field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, options)
87
- attr_order << name unless name.in?(attr_order)
88
+ attr_order << name unless attr_order.include?(name)
88
89
  end
89
90
 
90
- def index_specs_with_primary_key
91
- if index_specs.any?(&:primary_key?)
92
- index_specs
91
+ def index_definitions_with_primary_key
92
+ if index_definitions.any?(&:primary_key?)
93
+ index_definitions
93
94
  else
94
- index_specs + [rails_default_primary_key]
95
+ index_definitions + [rails_default_primary_key]
95
96
  end
96
97
  end
97
98
 
@@ -102,7 +103,7 @@ module DeclareSchema
102
103
  private
103
104
 
104
105
  def rails_default_primary_key
105
- ::DeclareSchema::Model::IndexSpec.new(self, [primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexSpec::PRIMARY_KEY_NAME)
106
+ ::DeclareSchema::Model::IndexDefinition.new(self, [primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
106
107
  end
107
108
 
108
109
  # Extend belongs_to so that it creates a FieldSpec for the foreign key
@@ -166,9 +167,8 @@ module DeclareSchema
166
167
  # does not effect the attribute in any way - it just records the
167
168
  # metadata.
168
169
  def declare_attr_type(name, type, options = {})
169
- klass = DeclareSchema.to_class(type)
170
- attr_types[name] = DeclareSchema.to_class(type)
171
- klass.declared(self, name, options) if klass.respond_to?(:declared)
170
+ attr_types[name] = klass = DeclareSchema.to_class(type)
171
+ klass.try(:declared, self, name, options)
172
172
  end
173
173
 
174
174
  # Add field validations according to arguments in the
@@ -192,7 +192,37 @@ module DeclareSchema
192
192
  end
193
193
  end
194
194
 
195
- def add_formatting_for_field(name, type, _args)
195
+ def add_serialize_for_field(name, type, options)
196
+ if (serialize_class = options.delete(:serialize))
197
+ type == :string || type == :text or raise ArgumentError, "serialize field type must be :string or :text"
198
+ serialize_args = Array((serialize_class unless serialize_class == true))
199
+ serialize(name, *serialize_args)
200
+ if options.has_key?(:default)
201
+ options[:default] = serialized_default(name, serialize_class == true ? Object : serialize_class, options[:default])
202
+ end
203
+ end
204
+ end
205
+
206
+ def serialized_default(attr_name, class_name_or_coder, default)
207
+ # copied from https://github.com/rails/rails/blob/7d6cb950e7c0e31c2faaed08c81743439156c9f5/activerecord/lib/active_record/attribute_methods/serialization.rb#L70-L76
208
+ coder = if class_name_or_coder == ::JSON
209
+ ActiveRecord::Coders::JSON
210
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
211
+ class_name_or_coder
212
+ elsif Rails::VERSION::MAJOR >= 5
213
+ ActiveRecord::Coders::YAMLColumn.new(attr_name, class_name_or_coder)
214
+ else
215
+ ActiveRecord::Coders::YAMLColumn.new(class_name_or_coder)
216
+ end
217
+
218
+ if default == coder.load(nil)
219
+ nil # handle Array default: [] or Hash default: {}
220
+ else
221
+ coder.dump(default)
222
+ end
223
+ end
224
+
225
+ def add_formatting_for_field(name, type)
196
226
  if (type_class = DeclareSchema.to_class(type))
197
227
  if "format".in?(type_class.instance_methods)
198
228
  before_validation do |record|
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class ForeignKeyDefinition
6
+ include Comparable
7
+
8
+ attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
9
+
10
+ def initialize(model, foreign_key, options = {})
11
+ @model = model
12
+ @foreign_key = foreign_key.presence
13
+ @options = options
14
+
15
+ @child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
16
+ @parent_table_name = options[:parent_table]
17
+ @foreign_key_name = options[:foreign_key] || self.foreign_key
18
+ @index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
19
+ @constraint_name = options[:constraint_name] || @index_name || ''
20
+ @on_delete_cascade = options[:dependent] == :delete
21
+
22
+ # Empty constraint lets mysql generate the name
23
+ end
24
+
25
+ class << self
26
+ def for_model(model, old_table_name)
27
+ show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
28
+ constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
29
+
30
+ constraints.map do |fkc|
31
+ options = {}
32
+ name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
33
+ options[:constraint_name] = name
34
+ options[:parent_table] = parent_table
35
+ options[:foreign_key] = foreign_key
36
+ options[:dependent] = :delete if fkc['ON DELETE CASCADE']
37
+
38
+ new(model, foreign_key, options)
39
+ end
40
+ end
41
+ end
42
+
43
+ def parent_table_name
44
+ @parent_table_name ||=
45
+ if (klass = options[:class_name])
46
+ klass = klass.to_s.constantize unless klass.is_a?(Class)
47
+ klass.try(:table_name)
48
+ end || foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
49
+ end
50
+
51
+ attr_writer :parent_table_name
52
+
53
+ def to_add_statement
54
+ statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
55
+ "execute #{statement.inspect}"
56
+ end
57
+
58
+ def key
59
+ @key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
60
+ end
61
+
62
+ def hash
63
+ key.hash
64
+ end
65
+
66
+ def <=>(rhs)
67
+ key <=> rhs.key
68
+ end
69
+
70
+ alias eql? ==
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class IndexDefinition
6
+ include Comparable
7
+
8
+ # TODO: replace `fields` with `columns` and remove alias. -Colin
9
+ attr_reader :table, :fields, :explicit_name, :name, :unique, :where
10
+ alias columns fields
11
+
12
+ class IndexNameTooLongError < RuntimeError; end
13
+
14
+ PRIMARY_KEY_NAME = "PRIMARY"
15
+ MYSQL_INDEX_NAME_MAX_LENGTH = 64
16
+
17
+ def initialize(model, fields, options = {})
18
+ @model = model
19
+ @table = options.delete(:table_name) || model.table_name
20
+ @fields = Array.wrap(fields).map(&:to_s)
21
+ @explicit_name = options[:name] unless options.delete(:allow_equivalent)
22
+ @name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
23
+ @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
24
+
25
+ if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
26
+ raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
27
+ end
28
+
29
+ if (where = options[:where])
30
+ @where = where.start_with?('(') ? where : "(#{where})"
31
+ end
32
+ end
33
+
34
+ class << self
35
+ # extract IndexSpecs from an existing table
36
+ # always includes the PRIMARY KEY index
37
+ def for_model(model, old_table_name = nil)
38
+ t = old_table_name || model.table_name
39
+
40
+ primary_key_columns = Array(model.connection.primary_key(t)).presence || begin
41
+ cols = model.connection.columns(t)
42
+ Array(
43
+ if cols.any? { |col| col.name == 'id' }
44
+ 'id'
45
+ else
46
+ cols.find { |col| col.type.to_s.include?('int') }&.name or raise "could not guess primary key for #{t} in #{cols.inspect}"
47
+ end
48
+ )
49
+ end
50
+ primary_key_found = false
51
+ index_definitions = model.connection.indexes(t).map do |i|
52
+ model.ignore_indexes.include?(i.name) and next
53
+ if i.name == PRIMARY_KEY_NAME
54
+ i.columns == primary_key_columns && i.unique or
55
+ raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
56
+ primary_key_found = true
57
+ elsif i.columns == primary_key_columns && i.unique
58
+ raise "found primary key index on #{t}.#{primary_key_columns} but it was called #{i.name}"
59
+ end
60
+ new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
61
+ end.compact
62
+
63
+ if !primary_key_found
64
+ index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
65
+ end
66
+ index_definitions
67
+ end
68
+ end
69
+
70
+ def primary_key?
71
+ name == PRIMARY_KEY_NAME
72
+ end
73
+
74
+ def to_add_statement(new_table_name, existing_primary_key = nil)
75
+ if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
76
+ to_add_primary_key_statement(new_table_name, existing_primary_key)
77
+ else
78
+ # Note: + below keeps that interpolated string from being frozen, so we can << into it.
79
+ r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
80
+ r << ", unique: true" if unique
81
+ r << ", where: '#{where}'" if where.present?
82
+ r << ", name: '#{name}'"
83
+ r
84
+ end
85
+ end
86
+
87
+ def to_add_primary_key_statement(new_table_name, existing_primary_key)
88
+ drop = "DROP PRIMARY KEY, " if existing_primary_key
89
+ statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
90
+ "execute #{statement.inspect}"
91
+ end
92
+
93
+ def to_key
94
+ @key ||= [table, fields, name, unique, where].map(&:to_s)
95
+ end
96
+
97
+ def settings
98
+ @settings ||= [table, fields, unique].map(&:to_s)
99
+ end
100
+
101
+ def hash
102
+ to_key.hash
103
+ end
104
+
105
+ def <=>(rhs)
106
+ to_key <=> rhs.to_key
107
+ end
108
+
109
+ def equivalent?(rhs)
110
+ settings == rhs.settings
111
+ end
112
+
113
+ def with_name(new_name)
114
+ self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
115
+ end
116
+
117
+ alias eql? ==
118
+ end
119
+ end
120
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -45,14 +45,14 @@ module Generators
45
45
  false # no single-column primary key
46
46
  end
47
47
 
48
- def index_specs_with_primary_key
48
+ def index_definitions_with_primary_key
49
49
  [
50
- ::DeclareSchema::Model::IndexSpec.new(self, foreign_keys, unique: true, name: "PRIMARY_KEY"),
51
- ::DeclareSchema::Model::IndexSpec.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
50
+ ::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME),
51
+ ::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
52
52
  ]
53
53
  end
54
54
 
55
- alias_method :index_specs, :index_specs_with_primary_key
55
+ alias_method :index_definitions, :index_definitions_with_primary_key
56
56
 
57
57
  def ignore_indexes
58
58
  []
@@ -60,8 +60,8 @@ module Generators
60
60
 
61
61
  def constraint_specs
62
62
  [
63
- ::DeclareSchema::Model::ForeignKeySpec.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
64
- ::DeclareSchema::Model::ForeignKeySpec.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
63
+ ::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
64
+ ::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
65
65
  ]
66
66
  end
67
67
  end
@@ -341,7 +341,7 @@ module Generators
341
341
  end
342
342
 
343
343
  def create_indexes(model)
344
- model.index_specs.map { |i| i.to_add_statement(model.table_name) }
344
+ model.index_definitions.map { |i| i.to_add_statement(model.table_name) }
345
345
  end
346
346
 
347
347
  def create_constraints(model)
@@ -449,8 +449,8 @@ module Generators
449
449
  return [[], []] if Migrator.disable_constraints
450
450
 
451
451
  new_table_name = model.table_name
452
- existing_indexes = ::DeclareSchema::Model::IndexSpec.for_model(model, old_table_name)
453
- model_indexes_with_equivalents = model.index_specs_with_primary_key
452
+ existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
453
+ model_indexes_with_equivalents = model.index_definitions_with_primary_key
454
454
  model_indexes = model_indexes_with_equivalents.map do |i|
455
455
  if i.explicit_name.nil?
456
456
  if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
@@ -458,20 +458,20 @@ module Generators
458
458
  end
459
459
  end || i
460
460
  end
461
- existing_has_primary_key = existing_indexes.any? { |i| i.name == 'PRIMARY_KEY' }
462
- model_has_primary_key = model_indexes.any? { |i| i.name == 'PRIMARY_KEY' }
461
+ existing_has_primary_key = existing_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
462
+ model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
463
463
 
464
464
  add_indexes_init = model_indexes - existing_indexes
465
465
  drop_indexes_init = existing_indexes - model_indexes
466
466
  undo_add_indexes = []
467
467
  undo_drop_indexes = []
468
468
  add_indexes = add_indexes_init.map do |i|
469
- undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == "PRIMARY_KEY"
469
+ undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
470
470
  i.to_add_statement(new_table_name, existing_has_primary_key)
471
471
  end
472
472
  drop_indexes = drop_indexes_init.map do |i|
473
473
  undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
474
- drop_index(new_table_name, i.name) unless i.name == "PRIMARY_KEY"
474
+ drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
475
475
  end.compact
476
476
 
477
477
  # the order is important here - adding a :unique, for instance needs to remove then add
@@ -489,7 +489,7 @@ module Generators
489
489
  return [[], []] if Migrator.disable_indexing
490
490
 
491
491
  new_table_name = model.table_name
492
- existing_fks = ::DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
492
+ existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
493
493
  model_fks = model.constraint_specs
494
494
  add_fks = model_fks - existing_fks
495
495
  drop_fks = existing_fks - model_fks
@@ -31,21 +31,23 @@ RSpec.describe 'DeclareSchema Migration Generator interactive primary key' do
31
31
 
32
32
  ### migrate to
33
33
 
34
- # rename to custom primary_key
35
- class Foo < ActiveRecord::Base
36
- fields do
34
+ if Rails::VERSION::MAJOR >= 5
35
+ # rename to custom primary_key
36
+ class Foo < ActiveRecord::Base
37
+ fields do
38
+ end
39
+ self.primary_key = "foo_id"
37
40
  end
38
- self.primary_key = "foo_id"
39
- end
40
41
 
41
- puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
42
- generate_migrations '-n', '-m'
43
- expect(Foo.primary_key).to eq('foo_id')
42
+ puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
43
+ generate_migrations '-n', '-m'
44
+ expect(Foo.primary_key).to eq('foo_id')
44
45
 
45
- ### ensure it doesn't cause further migrations
46
+ ### ensure it doesn't cause further migrations
46
47
 
47
- # check no further migrations
48
- up, down = Generators::DeclareSchema::Migration::Migrator.run
49
- expect(up).to eq("")
48
+ # check no further migrations
49
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
50
+ expect(up).to eq("")
51
+ end
50
52
  end
51
53
  end
@@ -51,19 +51,19 @@ RSpec.describe 'DeclareSchema Migration Generator' do
51
51
  end
52
52
  end
53
53
 
54
+ Advert.connection.schema_cache.clear!
55
+ Advert.reset_column_information
56
+
54
57
  expect(migrate).to(
55
58
  migrate_up(<<~EOS.strip)
56
59
  add_column :adverts, :body, :text
57
60
  add_column :adverts, :published_at, :datetime
58
-
59
- add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
60
61
  EOS
61
62
  .and migrate_down(<<~EOS.strip)
62
63
  remove_column :adverts, :body
63
64
  remove_column :adverts, :published_at
64
65
  EOS
65
66
  )
66
- # TODO: ^ TECH-4975 add_index should not be there
67
67
 
68
68
  Advert.field_specs.clear # not normally needed
69
69
  class Advert < ActiveRecord::Base
@@ -329,7 +329,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
329
329
  )
330
330
 
331
331
  Advert.field_specs.delete(:category_id)
332
- Advert.index_specs.delete_if { |spec| spec.fields==["category_id"] }
332
+ Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
333
333
 
334
334
  # If you specify a custom foreign key, the migration generator observes that:
335
335
 
@@ -348,7 +348,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
348
348
  )
349
349
 
350
350
  Advert.field_specs.delete(:c_id)
351
- Advert.index_specs.delete_if { |spec| spec.fields == ["c_id"] }
351
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
352
352
 
353
353
  # You can avoid generating the index by specifying `index: false`
354
354
 
@@ -365,7 +365,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
365
365
  )
366
366
 
367
367
  Advert.field_specs.delete(:category_id)
368
- Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
368
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
369
369
 
370
370
  # You can specify the index name with :index
371
371
 
@@ -384,7 +384,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
384
384
  )
385
385
 
386
386
  Advert.field_specs.delete(:category_id)
387
- Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
387
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
388
388
 
389
389
  ### Timestamps and Optimimistic Locking
390
390
 
@@ -433,7 +433,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
433
433
  EOS
434
434
  )
435
435
 
436
- Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
436
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
437
437
 
438
438
  # You can ask for a unique index
439
439
 
@@ -451,7 +451,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
451
451
  EOS
452
452
  )
453
453
 
454
- Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
454
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
455
455
 
456
456
  # You can specify the name for the index
457
457
 
@@ -469,7 +469,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
469
469
  EOS
470
470
  )
471
471
 
472
- Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
472
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
473
473
 
474
474
  # You can ask for an index outside of the fields block
475
475
 
@@ -485,7 +485,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
485
485
  EOS
486
486
  )
487
487
 
488
- Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
488
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
489
489
 
490
490
  # The available options for the index function are `:unique` and `:name`
491
491
 
@@ -501,7 +501,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
501
501
  EOS
502
502
  )
503
503
 
504
- Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
504
+ Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
505
505
 
506
506
  # You can create an index on more than one field
507
507
 
@@ -517,7 +517,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
517
517
  EOS
518
518
  )
519
519
 
520
- Advert.index_specs.delete_if { |spec| spec.fields==["title", "category_id"] }
520
+ Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
521
521
 
522
522
  # Finally, you can specify that the migration generator should completely ignore an
523
523
  # index by passing its name to ignore_index in the model.
@@ -545,7 +545,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
545
545
  add_column :ads, :title, :string, limit: 255
546
546
  add_column :ads, :body, :text
547
547
 
548
- add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
548
+ add_index :ads, [:id], unique: true, name: 'PRIMARY'
549
549
  EOS
550
550
  .and migrate_down(<<~EOS.strip)
551
551
  remove_column :ads, :title
@@ -553,7 +553,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
553
553
 
554
554
  rename_table :ads, :adverts
555
555
 
556
- add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
556
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY'
557
557
  EOS
558
558
  )
559
559
 
@@ -588,7 +588,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
588
588
  add_column :advertisements, :body, :text
589
589
  remove_column :advertisements, :name
590
590
 
591
- add_index :advertisements, [:id], unique: true, name: 'PRIMARY_KEY'
591
+ add_index :advertisements, [:id], unique: true, name: 'PRIMARY'
592
592
  EOS
593
593
  .and migrate_down(<<~EOS.strip)
594
594
  remove_column :advertisements, :title
@@ -597,7 +597,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
597
597
 
598
598
  rename_table :advertisements, :adverts
599
599
 
600
- add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
600
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY'
601
601
  EOS
602
602
  )
603
603
 
@@ -614,14 +614,11 @@ RSpec.describe 'DeclareSchema Migration Generator' do
614
614
  t.integer "id", limit: 8
615
615
  t.string "name", limit: 255
616
616
  end
617
-
618
- add_index "adverts", ["id"], name: "PRIMARY_KEY", unique: true
619
617
  EOS
620
618
 
621
619
  rails5_table_create = <<~EOS.strip
622
620
  create_table "adverts", id: :integer, force: :cascade do |t|
623
621
  t.string "name", limit: 255
624
- t.index ["id"], name: "PRIMARY_KEY", unique: true
625
622
  end
626
623
  EOS
627
624
 
@@ -670,7 +667,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
670
667
  Advert.field_specs.delete(:type)
671
668
  nuke_model_class(SuperFancyAdvert)
672
669
  nuke_model_class(FancyAdvert)
673
- Advert.index_specs.delete_if { |spec| spec.fields==["type"] }
670
+ Advert.index_definitions.delete_if { |spec| spec.fields==["type"] }
674
671
 
675
672
  ## Coping with multiple changes
676
673
 
@@ -728,7 +725,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
728
725
  add_column :ads, :created_at, :datetime, null: false
729
726
  change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
730
727
 
731
- add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
728
+ add_index :ads, [:id], unique: true, name: 'PRIMARY'
732
729
  EOS
733
730
  )
734
731
 
@@ -756,7 +753,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
756
753
  migrate_up(<<~EOS.strip)
757
754
  rename_column :adverts, :id, :advert_id
758
755
 
759
- add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY_KEY'
756
+ add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY'
760
757
  EOS
761
758
  )
762
759
 
@@ -797,6 +794,190 @@ RSpec.describe 'DeclareSchema Migration Generator' do
797
794
  expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
798
795
  end
799
796
 
797
+ describe 'serialize' do
798
+ before do
799
+ class Ad < ActiveRecord::Base
800
+ @serialize_args = []
801
+
802
+ class << self
803
+ attr_reader :serialize_args
804
+
805
+ def serialize(*args)
806
+ @serialize_args << args
807
+ end
808
+ end
809
+ end
810
+ end
811
+
812
+ describe 'untyped' do
813
+ it 'allows serialize: true' do
814
+ class Ad < ActiveRecord::Base
815
+ fields do
816
+ allow_list :text, limit: 0xFFFF, serialize: true
817
+ end
818
+ end
819
+
820
+ expect(Ad.serialize_args).to eq([[:allow_list]])
821
+ end
822
+
823
+ it 'converts defaults with .to_yaml' do
824
+ class Ad < ActiveRecord::Base
825
+ fields do
826
+ allow_list :string, limit: 255, serialize: true, null: true, default: []
827
+ allow_hash :string, limit: 255, serialize: true, null: true, default: {}
828
+ allow_string :string, limit: 255, serialize: true, null: true, default: ['abc']
829
+ allow_null :string, limit: 255, serialize: true, null: true, default: nil
830
+ end
831
+ end
832
+
833
+ expect(Ad.field_specs['allow_list'].default).to eq("--- []\n")
834
+ expect(Ad.field_specs['allow_hash'].default).to eq("--- {}\n")
835
+ expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
836
+ expect(Ad.field_specs['allow_null'].default).to eq(nil)
837
+ end
838
+ end
839
+
840
+ describe 'Array' do
841
+ it 'allows serialize: Array' do
842
+ class Ad < ActiveRecord::Base
843
+ fields do
844
+ allow_list :string, limit: 255, serialize: Array, null: true
845
+ end
846
+ end
847
+
848
+ expect(Ad.serialize_args).to eq([[:allow_list, Array]])
849
+ end
850
+
851
+ it 'allows Array defaults' do
852
+ class Ad < ActiveRecord::Base
853
+ fields do
854
+ allow_list :string, limit: 255, serialize: Array, null: true, default: [2]
855
+ allow_string :string, limit: 255, serialize: Array, null: true, default: ['abc']
856
+ allow_empty :string, limit: 255, serialize: Array, null: true, default: []
857
+ allow_null :string, limit: 255, serialize: Array, null: true, default: nil
858
+ end
859
+ end
860
+
861
+ expect(Ad.field_specs['allow_list'].default).to eq("---\n- 2\n")
862
+ expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
863
+ expect(Ad.field_specs['allow_empty'].default).to eq(nil)
864
+ expect(Ad.field_specs['allow_null'].default).to eq(nil)
865
+ end
866
+ end
867
+
868
+ describe 'Hash' do
869
+ it 'allows serialize: Hash' do
870
+ class Ad < ActiveRecord::Base
871
+ fields do
872
+ allow_list :string, limit: 255, serialize: Hash, null: true
873
+ end
874
+ end
875
+
876
+ expect(Ad.serialize_args).to eq([[:allow_list, Hash]])
877
+ end
878
+
879
+ it 'allows Hash defaults' do
880
+ class Ad < ActiveRecord::Base
881
+ fields do
882
+ allow_loc :string, limit: 255, serialize: Hash, null: true, default: { 'state' => 'CA' }
883
+ allow_hash :string, limit: 255, serialize: Hash, null: true, default: {}
884
+ allow_null :string, limit: 255, serialize: Hash, null: true, default: nil
885
+ end
886
+ end
887
+
888
+ expect(Ad.field_specs['allow_loc'].default).to eq("---\nstate: CA\n")
889
+ expect(Ad.field_specs['allow_hash'].default).to eq(nil)
890
+ expect(Ad.field_specs['allow_null'].default).to eq(nil)
891
+ end
892
+ end
893
+
894
+ describe 'JSON' do
895
+ it 'allows serialize: JSON' do
896
+ class Ad < ActiveRecord::Base
897
+ fields do
898
+ allow_list :string, limit: 255, serialize: JSON
899
+ end
900
+ end
901
+
902
+ expect(Ad.serialize_args).to eq([[:allow_list, JSON]])
903
+ end
904
+
905
+ it 'allows JSON defaults' do
906
+ class Ad < ActiveRecord::Base
907
+ fields do
908
+ allow_hash :string, limit: 255, serialize: JSON, null: true, default: { 'state' => 'CA' }
909
+ allow_empty_array :string, limit: 255, serialize: JSON, null: true, default: []
910
+ allow_empty_hash :string, limit: 255, serialize: JSON, null: true, default: {}
911
+ allow_null :string, limit: 255, serialize: JSON, null: true, default: nil
912
+ end
913
+ end
914
+
915
+ expect(Ad.field_specs['allow_hash'].default).to eq("{\"state\":\"CA\"}")
916
+ expect(Ad.field_specs['allow_empty_array'].default).to eq("[]")
917
+ expect(Ad.field_specs['allow_empty_hash'].default).to eq("{}")
918
+ expect(Ad.field_specs['allow_null'].default).to eq(nil)
919
+ end
920
+ end
921
+
922
+ class ValueClass
923
+ delegate :present?, :inspect, to: :@value
924
+
925
+ def initialize(value)
926
+ @value = value
927
+ end
928
+
929
+ class << self
930
+ def dump(object)
931
+ if object&.present?
932
+ object.inspect
933
+ end
934
+ end
935
+
936
+ def load(serialized)
937
+ if serialized
938
+ raise 'not used ???'
939
+ end
940
+ end
941
+ end
942
+ end
943
+
944
+ describe 'custom coder' do
945
+ it 'allows serialize: ValueClass' do
946
+ class Ad < ActiveRecord::Base
947
+ fields do
948
+ allow_list :string, limit: 255, serialize: ValueClass
949
+ end
950
+ end
951
+
952
+ expect(Ad.serialize_args).to eq([[:allow_list, ValueClass]])
953
+ end
954
+
955
+ it 'allows ValueClass defaults' do
956
+ class Ad < ActiveRecord::Base
957
+ fields do
958
+ allow_hash :string, limit: 255, serialize: ValueClass, null: true, default: ValueClass.new([2])
959
+ allow_empty_array :string, limit: 255, serialize: ValueClass, null: true, default: ValueClass.new([])
960
+ allow_null :string, limit: 255, serialize: ValueClass, null: true, default: nil
961
+ end
962
+ end
963
+
964
+ expect(Ad.field_specs['allow_hash'].default).to eq("[2]")
965
+ expect(Ad.field_specs['allow_empty_array'].default).to eq(nil)
966
+ expect(Ad.field_specs['allow_null'].default).to eq(nil)
967
+ end
968
+ end
969
+
970
+ it 'disallows serialize: with a non-string column type' do
971
+ expect do
972
+ class Ad < ActiveRecord::Base
973
+ fields do
974
+ allow_list :integer, limit: 8, serialize: true
975
+ end
976
+ end
977
+ end.to raise_exception(ArgumentError, /must be :string or :text/)
978
+ end
979
+ end
980
+
800
981
  context "for Rails #{Rails::VERSION::MAJOR}" do
801
982
  if Rails::VERSION::MAJOR >= 5
802
983
  let(:optional_true) { { optional: true } }
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../lib/declare_schema/model/index_definition'
4
+
5
+ # Beware: It looks out that Rails' sqlite3 driver has a bug in retrieving indexes.
6
+ # In sqlite3/schema_statements, it skips over any index that starts with sqlite_:
7
+ # next if row["name"].starts_with?("sqlite_")
8
+ # but this will skip over any indexes created to support "unique" column constraints.
9
+ # This gem provides an explicit name for all indexes so it shouldn't be affected by the bug...
10
+ # unless you manually create any Sqlite tables with UNIQUE constraints.
11
+
12
+ RSpec.describe DeclareSchema::Model::IndexDefinition do
13
+ before do
14
+ load File.expand_path('../prepare_testapp.rb', __dir__)
15
+
16
+ class IndexSettingsTestModel < ActiveRecord::Base
17
+ fields do
18
+ name :string, limit: 127, index: true
19
+
20
+ timestamps
21
+ end
22
+ end
23
+ end
24
+
25
+ let(:model_class) { IndexSettingsTestModel }
26
+
27
+ describe 'instance methods' do
28
+ let(:model) { model_class.new }
29
+ subject { declared_class.new(model_class) }
30
+
31
+ it 'has index_definitions' do
32
+ expect(model_class.index_definitions).to be_kind_of(Array)
33
+ expect(model_class.index_definitions.map(&:name)).to eq(['on_name'])
34
+ expect([:name, :fields, :unique].map { |attr| model_class.index_definitions[0].send(attr)}).to eq(
35
+ ['on_name', ['name'], false]
36
+ )
37
+ end
38
+
39
+ it 'has index_definitions_with_primary_key' do
40
+ expect(model_class.index_definitions_with_primary_key).to be_kind_of(Array)
41
+ result = model_class.index_definitions_with_primary_key.sort_by(&:name)
42
+ expect(result.map(&:name)).to eq(['PRIMARY', 'on_name'])
43
+ expect([:name, :fields, :unique].map { |attr| result[0].send(attr)}).to eq(
44
+ ['PRIMARY', ['id'], true]
45
+ )
46
+ expect([:name, :fields, :unique].map { |attr| result[1].send(attr)}).to eq(
47
+ ['on_name', ['name'], false]
48
+ )
49
+ end
50
+ end
51
+
52
+ describe 'class << self' do
53
+ context 'with a migrated database' do
54
+ before do
55
+ ActiveRecord::Base.connection.execute <<~EOS
56
+ CREATE TABLE index_settings_test_models (
57
+ id INTEGER NOT NULL PRIMARY KEY,
58
+ name TEXT NOT NULL
59
+ )
60
+ EOS
61
+ ActiveRecord::Base.connection.execute <<~EOS
62
+ CREATE UNIQUE INDEX index_settings_test_models_on_name ON index_settings_test_models(name)
63
+ EOS
64
+ ActiveRecord::Base.connection.schema_cache.clear!
65
+ end
66
+
67
+ describe 'for_model' do
68
+ subject { described_class.for_model(model_class) }
69
+
70
+ it 'returns the indexes for the model' do
71
+ expect(subject.size).to eq(2), subject.inspect
72
+ expect([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
73
+ ['index_settings_test_models_on_name', ['name'], true]
74
+ )
75
+ expect([:name, :columns, :unique].map { |attr| subject[1].send(attr) }).to eq(
76
+ ['PRIMARY', ['id'], true]
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ # TODO: fill out remaining tests
83
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: declare_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca Development adapted from hobo_fields by Tom Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-13 00:00:00.000000000 Z
11
+ date: 2020-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -59,7 +59,8 @@ files:
59
59
  - lib/declare_schema/field_declaration_dsl.rb
60
60
  - lib/declare_schema/model.rb
61
61
  - lib/declare_schema/model/field_spec.rb
62
- - lib/declare_schema/model/index_spec.rb
62
+ - lib/declare_schema/model/foreign_key_definition.rb
63
+ - lib/declare_schema/model/index_definition.rb
63
64
  - lib/declare_schema/railtie.rb
64
65
  - lib/declare_schema/version.rb
65
66
  - lib/generators/declare_schema/migration/USAGE
@@ -76,6 +77,7 @@ files:
76
77
  - spec/lib/declare_schema/generator_spec.rb
77
78
  - spec/lib/declare_schema/interactive_primary_key_spec.rb
78
79
  - spec/lib/declare_schema/migration_generator_spec.rb
80
+ - spec/lib/declare_schema/model/index_definition_spec.rb
79
81
  - spec/lib/declare_schema/prepare_testapp.rb
80
82
  - spec/lib/generators/declare_schema/migration/migrator_spec.rb
81
83
  - spec/spec_helper.rb
@@ -1,175 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DeclareSchema
4
- module Model
5
- class IndexSpec
6
- include Comparable
7
-
8
- attr_reader :table, :fields, :explicit_name, :name, :unique, :where
9
-
10
- class IndexNameTooLongError < RuntimeError; end
11
-
12
- PRIMARY_KEY_NAME = "PRIMARY_KEY"
13
- MYSQL_INDEX_NAME_MAX_LENGTH = 64
14
-
15
- def initialize(model, fields, options = {})
16
- @model = model
17
- @table = options.delete(:table_name) || model.table_name
18
- @fields = Array.wrap(fields).map(&:to_s)
19
- @explicit_name = options[:name] unless options.delete(:allow_equivalent)
20
- @name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
21
- @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
22
-
23
- if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
24
- raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
25
- end
26
-
27
- if (where = options[:where])
28
- @where = where.start_with?('(') ? where : "(#{where})"
29
- end
30
- end
31
-
32
- class << self
33
- # extract IndexSpecs from an existing table
34
- def for_model(model, old_table_name = nil)
35
- t = old_table_name || model.table_name
36
- connection = model.connection.dup
37
- # TODO: Change below to use prepend
38
- class << connection # defeat Rails code that skips the primary keys by changing their name to PRIMARY_KEY_NAME
39
- def each_hash(result)
40
- super do |hash|
41
- if hash[:Key_name] == "PRIMARY"
42
- hash[:Key_name] = PRIMARY_KEY_NAME
43
- end
44
- yield hash
45
- end
46
- end
47
- end
48
- connection.indexes(t).map do |i|
49
- new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name) unless model.ignore_indexes.include?(i.name)
50
- end.compact
51
- end
52
- end
53
-
54
- def primary_key?
55
- name == PRIMARY_KEY_NAME
56
- end
57
-
58
- def to_add_statement(new_table_name, existing_primary_key = nil)
59
- if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
60
- to_add_primary_key_statement(new_table_name, existing_primary_key)
61
- else
62
- r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
63
- r += ", unique: true" if unique
64
- r += ", where: '#{where}'" if where.present?
65
- r += ", name: '#{name}'"
66
- r
67
- end
68
- end
69
-
70
- def to_add_primary_key_statement(new_table_name, existing_primary_key)
71
- drop = "DROP PRIMARY KEY, " if existing_primary_key
72
- statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
73
- "execute #{statement.inspect}"
74
- end
75
-
76
- def to_key
77
- @key ||= [table, fields, name, unique, where].map(&:to_s)
78
- end
79
-
80
- def settings
81
- @settings ||= [table, fields, unique].map(&:to_s)
82
- end
83
-
84
- def hash
85
- to_key.hash
86
- end
87
-
88
- def <=>(rhs)
89
- to_key <=> rhs.to_key
90
- end
91
-
92
- def equivalent?(rhs)
93
- settings == rhs.settings
94
- end
95
-
96
- def with_name(new_name)
97
- self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
98
- end
99
-
100
- alias eql? ==
101
- end
102
-
103
- class ForeignKeySpec
104
- include Comparable
105
-
106
- attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
107
-
108
- def initialize(model, foreign_key, options = {})
109
- @model = model
110
- @foreign_key = foreign_key.presence
111
- @options = options
112
-
113
- @child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
114
- @parent_table_name = options[:parent_table]
115
- @foreign_key_name = options[:foreign_key] || self.foreign_key
116
- @index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
117
- @constraint_name = options[:constraint_name] || @index_name || ''
118
- @on_delete_cascade = options[:dependent] == :delete
119
-
120
- # Empty constraint lets mysql generate the name
121
- end
122
-
123
- class << self
124
- def for_model(model, old_table_name)
125
- show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
126
- constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
127
-
128
- constraints.map do |fkc|
129
- options = {}
130
- name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
131
- options[:constraint_name] = name
132
- options[:parent_table] = parent_table
133
- options[:foreign_key] = foreign_key
134
- options[:dependent] = :delete if fkc['ON DELETE CASCADE']
135
-
136
- new(model, foreign_key, options)
137
- end
138
- end
139
- end
140
-
141
- def parent_table_name
142
- @parent_table_name ||=
143
- options[:class_name]&.is_a?(Class) &&
144
- options[:class_name].respond_to?(:table_name) &&
145
- options[:class_name]&.table_name
146
- @parent_table_name ||=
147
- options[:class_name]&.constantize &&
148
- options[:class_name].constantize.respond_to?(:table_name) &&
149
- options[:class_name].constantize.table_name ||
150
- foreign_key.gsub(/_id/, '').camelize.constantize.table_name
151
- end
152
-
153
- attr_writer :parent_table_name
154
-
155
- def to_add_statement(_ = true)
156
- statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
157
- "execute #{statement.inspect}"
158
- end
159
-
160
- def to_key
161
- @key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
162
- end
163
-
164
- def hash
165
- to_key.hash
166
- end
167
-
168
- def <=>(rhs)
169
- to_key <=> rhs.to_key
170
- end
171
-
172
- alias eql? ==
173
- end
174
- end
175
- end