declare_schema 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +21 -5
  3. data/Appraisals +21 -4
  4. data/CHANGELOG.md +40 -0
  5. data/Gemfile +1 -2
  6. data/Gemfile.lock +7 -9
  7. data/README.md +3 -3
  8. data/Rakefile +17 -4
  9. data/bin/declare_schema +1 -1
  10. data/declare_schema.gemspec +1 -1
  11. data/gemfiles/rails_4_mysql.gemfile +22 -0
  12. data/gemfiles/{rails_4.gemfile → rails_4_sqlite.gemfile} +1 -2
  13. data/gemfiles/rails_5_mysql.gemfile +22 -0
  14. data/gemfiles/{rails_5.gemfile → rails_5_sqlite.gemfile} +1 -2
  15. data/gemfiles/rails_6_mysql.gemfile +22 -0
  16. data/gemfiles/{rails_6.gemfile → rails_6_sqlite.gemfile} +2 -3
  17. data/lib/declare_schema/command.rb +10 -3
  18. data/lib/declare_schema/model/column.rb +168 -0
  19. data/lib/declare_schema/model/field_spec.rb +59 -143
  20. data/lib/declare_schema/model/foreign_key_definition.rb +36 -25
  21. data/lib/declare_schema/model/table_options_definition.rb +8 -6
  22. data/lib/declare_schema/version.rb +1 -1
  23. data/lib/generators/declare_schema/migration/migration_generator.rb +1 -1
  24. data/lib/generators/declare_schema/migration/migrator.rb +142 -116
  25. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +1 -1
  26. data/spec/lib/declare_schema/field_spec_spec.rb +135 -38
  27. data/spec/lib/declare_schema/generator_spec.rb +4 -2
  28. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +8 -2
  29. data/spec/lib/declare_schema/migration_generator_spec.rb +277 -171
  30. data/spec/lib/declare_schema/model/column_spec.rb +141 -0
  31. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +93 -0
  32. data/spec/lib/declare_schema/model/index_definition_spec.rb +4 -5
  33. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +19 -29
  34. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +12 -26
  35. data/spec/support/acceptance_spec_helpers.rb +3 -3
  36. metadata +15 -9
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ class UnknownSqlTypeError < RuntimeError; end
5
+
6
+ module Model
7
+ # This class is a wrapper for the ActiveRecord::...::Column class
8
+ class Column
9
+ class << self
10
+ def native_type?(type)
11
+ type != :primary_key && native_types.has_key?(type)
12
+ end
13
+
14
+ # MySQL example:
15
+ # { primary_key: "bigint auto_increment PRIMARY KEY",
16
+ # string: { name: "varchar", limit: 255 },
17
+ # text: { name: "text", limit: 65535},
18
+ # integer: {name: "int", limit: 4 },
19
+ # float: {name: "float", limit: 24 },
20
+ # decimal: { name: "decimal" },
21
+ # datetime: { name: "datetime" },
22
+ # timestamp: { name: "timestamp" },
23
+ # time: { name: "time" },
24
+ # date: { name: "date" },
25
+ # binary: { name>: "blob", limit: 65535 },
26
+ # boolean: { name: "tinyint", limit: 1 },
27
+ # json: { name: "json" } }
28
+ #
29
+ # SQLite example:
30
+ # { primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
31
+ # string: { name: "varchar" },
32
+ # text: { name: "text"},
33
+ # integer: { name: "integer" },
34
+ # float: { name: "float" },
35
+ # decimal: { name: "decimal" },
36
+ # datetime: { name: "datetime" },
37
+ # time: { name: "time" },
38
+ # date: { name: "date" },
39
+ # binary: { name: "blob" },
40
+ # boolean: { name: "boolean" },
41
+ # json: { name: "json" } }
42
+ def native_types
43
+ @native_types ||= ActiveRecord::Base.connection.native_database_types.tap do |types|
44
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
45
+ types[:text][:limit] ||= 0xffff
46
+ types[:binary][:limit] ||= 0xffff
47
+ end
48
+ end
49
+ end
50
+
51
+ def sql_type(type)
52
+ if native_type?(type)
53
+ type
54
+ else
55
+ if (field_class = DeclareSchema.to_class(type))
56
+ field_class::COLUMN_TYPE
57
+ end or raise UnknownSqlTypeError, "#{type.inspect} for type #{type.inspect}"
58
+ end
59
+ end
60
+
61
+ def deserialize_default_value(column, sql_type, default_value)
62
+ sql_type or raise ArgumentError, "must pass sql_type; got #{sql_type.inspect}"
63
+
64
+ case Rails::VERSION::MAJOR
65
+ when 4
66
+ # TODO: Delete this Rails 4 support ASAP! This could be wrong, since it's using the type of the old column...which
67
+ # might be getting migrated to a new type. We should be using just sql_type as below. -Colin
68
+ column.type_cast_from_database(default_value)
69
+ else
70
+ cast_type = ActiveRecord::Base.connection.send(:lookup_cast_type, sql_type) or
71
+ raise "cast_type not found for #{sql_type}"
72
+ cast_type.deserialize(default_value)
73
+ end
74
+ end
75
+
76
+ # Normalizes schema attributes for the specific database adapter that is currently running
77
+ # Note that the un-normalized attributes are still useful for generating migrations because those
78
+ # may be run with a different adapter.
79
+ # This method never mutates its argument. In fact it freezes it to be certain.
80
+ def normalize_schema_attributes(schema_attributes)
81
+ schema_attributes[:type] or raise ArgumentError, ":type key not found; keys: #{schema_attributes.keys.inspect}"
82
+ schema_attributes.freeze
83
+
84
+ case ActiveRecord::Base.connection.class.name
85
+ when /mysql/i
86
+ schema_attributes
87
+ when /sqlite/i
88
+ case schema_attributes[:type]
89
+ when :text
90
+ schema_attributes = schema_attributes.merge(limit: nil)
91
+ when :integer
92
+ schema_attributes = schema_attributes.dup
93
+ schema_attributes[:limit] ||= 8
94
+ end
95
+ schema_attributes
96
+ else
97
+ schema_attributes
98
+ end
99
+ end
100
+
101
+ def equivalent_schema_attributes?(schema_attributes_lhs, schema_attributes_rhs)
102
+ normalize_schema_attributes(schema_attributes_lhs) == normalize_schema_attributes(schema_attributes_rhs)
103
+ end
104
+ end
105
+
106
+ def initialize(model, current_table_name, column)
107
+ @model = model or raise ArgumentError, "must pass model"
108
+ @current_table_name = current_table_name or raise ArgumentError, "must pass current_table_name"
109
+ @column = column or raise ArgumentError, "must pass column"
110
+ end
111
+
112
+ def sql_type
113
+ @sql_type ||= self.class.sql_type(@column.type)
114
+ end
115
+
116
+ SCHEMA_KEYS = [:type, :limit, :precision, :scale, :null, :default].freeze
117
+
118
+ # omits keys with nil values
119
+ def schema_attributes
120
+ SCHEMA_KEYS.each_with_object({}) do |key, result|
121
+ value =
122
+ case key
123
+ when :default
124
+ self.class.deserialize_default_value(@column, sql_type, @column.default)
125
+ else
126
+ col_value = @column.send(key)
127
+ if col_value.nil? && (native_type = self.class.native_types[@column.type])
128
+ native_type[key]
129
+ else
130
+ col_value
131
+ end
132
+ end
133
+
134
+ result[key] = value unless value.nil?
135
+ end.tap do |result|
136
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i) && @column.type.in?([:string, :text])
137
+ result.merge!(collation_and_charset_for_column(@current_table_name, @column.name))
138
+ end
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def collation_and_charset_for_column(current_table_name, column_name)
145
+ connection = ActiveRecord::Base.connection
146
+ connection.class.name.match?(/mysql/i) or raise ArgumentError, "only supported for MySQL"
147
+
148
+ database_name = connection.current_database
149
+
150
+ defaults = connection.select_one(<<~EOS)
151
+ SELECT C.character_set_name, C.collation_name
152
+ FROM information_schema.`COLUMNS` C
153
+ WHERE C.table_schema = '#{connection.quote_string(database_name)}' AND
154
+ C.table_name = '#{connection.quote_string(current_table_name)}' AND
155
+ C.column_name = '#{connection.quote_string(column_name)}';
156
+ EOS
157
+
158
+ defaults && defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect} from #{database_name}.#{current_table_name}.#{column_name}"
159
+ defaults && defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
160
+
161
+ {
162
+ charset: defaults["character_set_name"],
163
+ collation: defaults["collation_name"]
164
+ }
165
+ end
166
+ end
167
+ end
168
+ end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'column'
4
+
3
5
  module DeclareSchema
6
+ class MysqlTextMayNotHaveDefault < RuntimeError; end
7
+
4
8
  module Model
5
9
  class FieldSpec
6
- class UnknownSqlTypeError < RuntimeError; end
7
10
 
8
11
  MYSQL_TINYTEXT_LIMIT = 0xff
9
12
  MYSQL_TEXT_LIMIT = 0xffff
@@ -13,7 +16,6 @@ module DeclareSchema
13
16
  MYSQL_TEXT_LIMITS_ASCENDING = [MYSQL_TINYTEXT_LIMIT, MYSQL_TEXT_LIMIT, MYSQL_MEDIUMTEXT_LIMIT, MYSQL_LONGTEXT_LIMIT].freeze
14
17
 
15
18
  class << self
16
- # method for easy stubbing in tests
17
19
  def mysql_text_limits?
18
20
  if defined?(@mysql_text_limits)
19
21
  @mysql_text_limits
@@ -31,7 +33,18 @@ module DeclareSchema
31
33
  end
32
34
  end
33
35
 
34
- attr_reader :model, :name, :type, :position, :options
36
+ attr_reader :model, :name, :type, :sql_type, :position, :options, :sql_options
37
+
38
+ TYPE_SYNONYMS = { timestamp: :datetime }.freeze # TODO: drop this synonym. -Colin
39
+
40
+ SQL_OPTIONS = [:limit, :precision, :scale, :null, :default, :charset, :collation].freeze
41
+ NON_SQL_OPTIONS = [:ruby_default, :validates].freeze
42
+ VALID_OPTIONS = (SQL_OPTIONS + NON_SQL_OPTIONS).freeze
43
+ OPTION_INDEXES = Hash[VALID_OPTIONS.each_with_index.to_a].freeze
44
+
45
+ VALID_OPTIONS.each do |option|
46
+ define_method(option) { @options[option] }
47
+ end
35
48
 
36
49
  def initialize(model, name, type, position: 0, **options)
37
50
  # TODO: TECH-5116
@@ -43,169 +56,72 @@ module DeclareSchema
43
56
  @model = model
44
57
  @name = name.to_sym
45
58
  type.is_a?(Symbol) or raise ArgumentError, "type must be a Symbol; got #{type.inspect}"
46
- @type = type
59
+ @type = TYPE_SYNONYMS[type] || type
47
60
  @position = position
48
- @options = options
61
+ @options = options.dup
62
+
63
+ @options.has_key?(:null) or @options[:null] = false
64
+
49
65
  case type
50
66
  when :text
51
- @options[:default] and raise "default may not be given for :text field #{model}##{@name}"
52
67
  if self.class.mysql_text_limits?
68
+ @options[:default].nil? or raise MysqlTextMayNotHaveDefault, "when using MySQL, non-nil default may not be given for :text field #{model}##{@name}"
53
69
  @options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit] || MYSQL_LONGTEXT_LIMIT)
70
+ else
71
+ @options[:limit] = nil
54
72
  end
55
73
  when :string
56
- @options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
74
+ @options[:limit] or raise "limit: must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
57
75
  when :bigint
58
76
  @type = :integer
59
- @options = options.merge(limit: 8)
77
+ @options[:limit] = 8
60
78
  end
61
79
 
62
- unless type.in?([:text, :string])
63
- @options[:collation] and raise "collation may only given for :string and :text fields"
64
- @options[:charset] and raise "charset may only given for :string and :text fields"
65
- end
66
- end
67
-
68
- TYPE_SYNONYMS = { timestamp: :datetime }.freeze
69
-
70
- SQLITE_COLUMN_CLASS =
71
- begin
72
- ActiveRecord::ConnectionAdapters::SQLiteColumn
73
- rescue NameError
74
- NilClass
75
- end
80
+ # TODO: Do we really need to support a :sql_type option? Ideally, drop it. -Colin
81
+ @sql_type = @options.delete(:sql_type) || Column.sql_type(@type)
76
82
 
77
- def sql_type
78
- @options[:sql_type] || begin
79
- if native_type?(type)
80
- type
81
- else
82
- field_class = DeclareSchema.to_class(type)
83
- field_class && field_class::COLUMN_TYPE or raise UnknownSqlTypeError, "#{type.inspect} for #{model}##{@name}"
84
- end
85
- end
86
- end
87
-
88
- def sql_options
89
- @options.except(:ruby_default, :validates)
90
- end
91
-
92
- def limit
93
- @options[:limit] || native_types[sql_type][:limit]
94
- end
95
-
96
- def precision
97
- @options[:precision]
98
- end
99
-
100
- def scale
101
- @options[:scale]
102
- end
103
-
104
- def null
105
- !:null.in?(@options) || @options[:null]
106
- end
107
-
108
- def default
109
- @options[:default]
110
- end
111
-
112
- def collation
113
- if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
114
- (@options[:collation] || model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation).to_s
83
+ if @sql_type.in?([:string, :text, :binary, :varbinary, :integer, :enum])
84
+ @options[:limit] ||= Column.native_types[@sql_type][:limit]
85
+ else
86
+ @sql_type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@sql_type} in field #{model}##{@name}")
87
+ @options.delete(:limit)
115
88
  end
116
- end
117
89
 
118
- def charset
119
- if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
120
- (@options[:charset] || model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset).to_s
90
+ if @sql_type == :decimal
91
+ @options[:precision] or warn("precision: required for :decimal type in field #{model}##{@name}")
92
+ @options[:scale] or warn("scale: required for :decimal type in field #{model}##{@name}")
93
+ else
94
+ if @sql_type != :datetime
95
+ @options.has_key?(:precision) and warn("precision: only allowed for :decimal type or :datetime for SQL type #{@sql_type} in field #{model}##{@name}")
96
+ end
97
+ @options.has_key?(:scale) and warn("scale: only allowed for :decimal type for SQL type #{@sql_type} in field #{model}##{@name}")
121
98
  end
122
- end
123
99
 
124
- def same_type?(col_spec)
125
- type = sql_type
126
- normalized_type = TYPE_SYNONYMS[type] || type
127
- normalized_col_spec_type = TYPE_SYNONYMS[col_spec.type] || col_spec.type
128
- normalized_type == normalized_col_spec_type
129
- end
130
-
131
- def different_to?(table_name, col_spec)
132
- !same_as(table_name, col_spec)
133
- end
134
-
135
- def same_as(table_name, col_spec)
136
- same_type?(col_spec) &&
137
- same_attributes?(col_spec) &&
138
- (!type.in?([:text, :string]) || same_charset_and_collation?(table_name, col_spec))
139
- end
140
-
141
- private
142
-
143
- def same_attributes?(col_spec)
144
- native_type = native_types[type]
145
- check_attributes = [:null, :default]
146
- check_attributes += [:precision, :scale] if sql_type == :decimal && !col_spec.is_a?(SQLITE_COLUMN_CLASS) # remove when rails fixes https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2872
147
- check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
148
- check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
149
- (sql_type == :text && self.class.mysql_text_limits?)
150
- check_attributes.all? do |k|
151
- if k == :default
152
- case Rails::VERSION::MAJOR
153
- when 4
154
- col_spec.type_cast_from_database(col_spec.default) == col_spec.type_cast_from_database(default)
155
- else
156
- cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
157
- cast_type.deserialize(col_spec.default) == cast_type.deserialize(default)
158
- end
100
+ if @type.in?([:text, :string])
101
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
102
+ @options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
103
+ @options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
159
104
  else
160
- col_value = col_spec.send(k)
161
- if col_value.nil? && native_type
162
- col_value = native_type[k]
163
- end
164
- col_value == send(k)
105
+ @options.delete(:charset)
106
+ @options.delete(:collation)
165
107
  end
166
- end
167
- end
168
-
169
- def same_charset_and_collation?(table_name, col_spec)
170
- current_collation_and_charset = collation_and_charset_for_column(table_name, col_spec)
171
-
172
- collation == current_collation_and_charset[:collation] &&
173
- charset == current_collation_and_charset[:charset]
174
- end
175
-
176
- def collation_and_charset_for_column(table_name, col_spec)
177
- column_name = col_spec.name
178
- connection = ActiveRecord::Base.connection
179
-
180
- if connection.class.name.match?(/mysql/i)
181
- database_name = connection.current_database
182
-
183
- defaults = connection.select_one(<<~EOS)
184
- SELECT C.character_set_name, C.collation_name
185
- FROM information_schema.`COLUMNS` C
186
- WHERE C.table_schema = '#{connection.quote_string(database_name)}' AND
187
- C.table_name = '#{connection.quote_string(table_name)}' AND
188
- C.column_name = '#{connection.quote_string(column_name)}';
189
- EOS
190
-
191
- defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
192
- defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
193
-
194
- {
195
- charset: defaults["character_set_name"],
196
- collation: defaults["collation_name"]
197
- }
198
108
  else
199
- {}
109
+ @options[:charset] and warn("charset may only given for :string and :text fields for SQL type #{@sql_type} in field #{model}##{@name}")
110
+ @options[:collation] and warne("collation may only given for :string and :text fields for SQL type #{@sql_type} in field #{model}##{@name}")
200
111
  end
201
- end
202
112
 
203
- def native_type?(type)
204
- type.to_sym != :primary_key && native_types.has_key?(type)
113
+ @options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]
114
+
115
+ @sql_options = @options.except(*NON_SQL_OPTIONS)
205
116
  end
206
117
 
207
- def native_types
208
- Generators::DeclareSchema::Migration::Migrator.native_types
118
+ # returns the attributes for schema migrations as a Hash
119
+ # omits name and position since those are meta-data above the schema
120
+ # omits keys with nil values
121
+ def schema_attributes(col_spec)
122
+ @options.merge(type: @type).tap do |attrs|
123
+ attrs[:default] = Column.deserialize_default_value(col_spec, @sql_type, attrs[:default])
124
+ end.compact
209
125
  end
210
126
  end
211
127
  end
@@ -5,21 +5,21 @@ module DeclareSchema
5
5
  class ForeignKeyDefinition
6
6
  include Comparable
7
7
 
8
- attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
8
+ attr_reader :constraint_name, :model, :foreign_key, :foreign_key_name, :options, :on_delete_cascade
9
9
 
10
10
  def initialize(model, foreign_key, options = {})
11
11
  @model = model
12
- @foreign_key = foreign_key.presence
12
+ @foreign_key = foreign_key.to_s.presence
13
13
  @options = options
14
14
 
15
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
16
+ @parent_table_name = options[:parent_table]&.to_s
17
+ @foreign_key_name = options[:foreign_key]&.to_s || @foreign_key
18
+ @index_name = options[:index_name]&.to_s || model.connection.index_name(model.table_name, column: @foreign_key_name)
21
19
 
22
20
  # Empty constraint lets mysql generate the name
21
+ @constraint_name = options[:constraint_name]&.to_s || @index_name&.to_s || ''
22
+ @on_delete_cascade = options[:dependent] == :delete
23
23
  end
24
24
 
25
25
  class << self
@@ -28,11 +28,12 @@ module DeclareSchema
28
28
  constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
29
29
 
30
30
  constraints.map do |fkc|
31
- options = {}
32
31
  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
32
+ options = {
33
+ constraint_name: name,
34
+ parent_table: parent_table,
35
+ foreign_key: foreign_key
36
+ }
36
37
  options[:dependent] = :delete if fkc['ON DELETE CASCADE']
37
38
 
38
39
  new(model, foreign_key, options)
@@ -40,21 +41,37 @@ module DeclareSchema
40
41
  end
41
42
  end
42
43
 
44
+ # returns the parent class as a Class object
45
+ # or nil if no :class_name option given
46
+ def parent_class
47
+ if (class_name = options[:class_name])
48
+ if class_name.is_a?(Class)
49
+ class_name
50
+ else
51
+ class_name.to_s.constantize
52
+ end
53
+ end
54
+ end
55
+
43
56
  def parent_table_name
44
57
  @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
58
+ parent_class&.try(:table_name) ||
59
+ foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
49
60
  end
50
61
 
51
- attr_writer :parent_table_name
52
-
53
62
  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}"
63
+ "add_foreign_key(#{@child_table.inspect}, #{parent_table_name.inspect}, " +
64
+ "column: #{@foreign_key_name.inspect}, name: #{@constraint_name.inspect})"
65
+ end
66
+
67
+ def <=>(rhs)
68
+ key <=> rhs.send(:key)
56
69
  end
57
70
 
71
+ alias eql? ==
72
+
73
+ private
74
+
58
75
  def key
59
76
  @key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
60
77
  end
@@ -62,12 +79,6 @@ module DeclareSchema
62
79
  def hash
63
80
  key.hash
64
81
  end
65
-
66
- def <=>(rhs)
67
- key <=> rhs.key
68
- end
69
-
70
- alias eql? ==
71
82
  end
72
83
  end
73
84
  end