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.
- checksums.yaml +4 -4
- data/.github/workflows/declare_schema_build.yml +21 -5
- data/Appraisals +21 -4
- data/CHANGELOG.md +40 -0
- data/Gemfile +1 -2
- data/Gemfile.lock +7 -9
- data/README.md +3 -3
- data/Rakefile +17 -4
- data/bin/declare_schema +1 -1
- data/declare_schema.gemspec +1 -1
- data/gemfiles/rails_4_mysql.gemfile +22 -0
- data/gemfiles/{rails_4.gemfile → rails_4_sqlite.gemfile} +1 -2
- data/gemfiles/rails_5_mysql.gemfile +22 -0
- data/gemfiles/{rails_5.gemfile → rails_5_sqlite.gemfile} +1 -2
- data/gemfiles/rails_6_mysql.gemfile +22 -0
- data/gemfiles/{rails_6.gemfile → rails_6_sqlite.gemfile} +2 -3
- data/lib/declare_schema/command.rb +10 -3
- data/lib/declare_schema/model/column.rb +168 -0
- data/lib/declare_schema/model/field_spec.rb +59 -143
- data/lib/declare_schema/model/foreign_key_definition.rb +36 -25
- data/lib/declare_schema/model/table_options_definition.rb +8 -6
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +142 -116
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +1 -1
- data/spec/lib/declare_schema/field_spec_spec.rb +135 -38
- data/spec/lib/declare_schema/generator_spec.rb +4 -2
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +8 -2
- data/spec/lib/declare_schema/migration_generator_spec.rb +277 -171
- data/spec/lib/declare_schema/model/column_spec.rb +141 -0
- data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +93 -0
- data/spec/lib/declare_schema/model/index_definition_spec.rb +4 -5
- data/spec/lib/declare_schema/model/table_options_definition_spec.rb +19 -29
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +12 -26
- data/spec/support/acceptance_spec_helpers.rb +3 -3
- 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 =
|
77
|
+
@options[:limit] = 8
|
60
78
|
end
|
61
79
|
|
62
|
-
|
63
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
161
|
-
|
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
|
-
|
204
|
-
|
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
|
-
|
208
|
-
|
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] ||
|
18
|
-
@index_name = options[:index_name] || model.connection.index_name(model.table_name, column:
|
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
|
34
|
-
|
35
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
55
|
-
|
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
|