declare_schema 0.6.0 → 0.7.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.
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