declare_schema 0.3.1 → 0.4.0

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: 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