declare_schema 0.6.4 → 0.8.0.pre.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 523660550181d173c4ff4d6dc181b6d790419a3fdfb1e956076eed2d4c9f7f3b
4
- data.tar.gz: '0380d2f800fd64c770c87631090ee792875034211b6ba9d18f5acc34e5ba9f0b'
3
+ metadata.gz: cd8228bfafad807a853f8abea954459b3ac209b7041b27699fc5691184c86b80
4
+ data.tar.gz: 01a278f8d7bbc83a9309a3c8908b546c509f5b2d14e28fe11bca2fa4fcc7bbed
5
5
  SHA512:
6
- metadata.gz: 824d98b6bb3b30d0eebf594db7cead5e954fed227eefdb8f627811da3a7f8c9bc76f62552c3c3a87b0ff3adf51a95a1e666e903d6993430aaa3521607b6ee260
7
- data.tar.gz: e8b45fb7dd6df1b314b3ada7e3da6223c631911ce88233165dff486ddb0bfc4a09e42255c018e7d00a456f95d0c1a8af010fe5531f6c2d64516cf3a09f51eb19
6
+ metadata.gz: 632350f71bb95b99fd7f25f0dd9a71c84d544e1e5d26cafcf3ae671e994c9719be3c7f4f297a096ffdca3ec63c5179aea1c159f600bbb061fa86b99a3d92fc6c
7
+ data.tar.gz: 757876168af2c046c63332fadc22ce26b63e7697ae5d6c9091b46a4c5d7d349eefc2899052e6e0f2d7df13a200c18f029d881937436e95a17283f1ce709f19f5
data/CHANGELOG.md CHANGED
@@ -4,11 +4,27 @@ 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.6.4] - 2020-02-08
7
+ ## [0.8.0] - UNRELEASED
8
+ ### Removed
9
+ - Removed `sql_type` that was confusing because it was actually the same as `type` (ex: :string) and not
10
+ in fact the SQL type (ex: ``varchar(255)'`).
11
+
12
+ ## [0.7.1] - 2021-02-17
13
+ ### Fixed
14
+ - Exclude unknown options from FieldSpec#sql_options and #schema_attributes.
15
+ - Fixed a bug where fk_field_options were getting merged into spec_attrs after checking for equivalence,
16
+ leading to phantom migrations with no changes, or missing migrations when just the fk_field_options changed.
17
+
18
+ ## [0.7.0] - 2021-02-14
19
+ ### Changed
20
+ - Use `schema_attributes` for generating both up and down change migrations, so they are guaranteed to be symmetrical.
21
+ Note: Rails schema dumper is still used for the down migration to replace a model that has been dropped.
22
+
23
+ ## [0.6.4] - 2021-02-08
8
24
  - Fixed a bug where the generated call to add_foreign_key() was not setting `column:`,
9
25
  so it only worked in cases where Rails could infer the foreign key by convention.
10
26
 
11
- ## [0.6.3] - 2020-01-21
27
+ ## [0.6.3] - 2021-01-21
12
28
  ### Added
13
29
  - Added `add_foreign_key` native rails call in `DeclareSchema::Model::ForeignKeyDefinition#to_add_statement`.
14
30
 
@@ -114,6 +130,9 @@ using the appropriate Rails configuration attributes.
114
130
  ### Added
115
131
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
116
132
 
133
+ [0.8.0]: https://github.com/Invoca/declare_schema/compare/v0.7.1...v0.8.0
134
+ [0.7.1]: https://github.com/Invoca/declare_schema/compare/v0.7.0...v0.7.1
135
+ [0.7.0]: https://github.com/Invoca/declare_schema/compare/v0.6.3...v0.7.0
117
136
  [0.6.4]: https://github.com/Invoca/declare_schema/compare/v0.6.3...v0.6.4
118
137
  [0.6.3]: https://github.com/Invoca/declare_schema/compare/v0.6.2...v0.6.3
119
138
  [0.6.2]: https://github.com/Invoca/declare_schema/compare/v0.6.1...v0.6.2
data/Gemfile.lock CHANGED
@@ -1,49 +1,49 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.6.4)
4
+ declare_schema (0.8.0.pre.3)
5
5
  rails (>= 4.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- actioncable (5.2.4.4)
11
- actionpack (= 5.2.4.4)
10
+ actioncable (5.2.4.5)
11
+ actionpack (= 5.2.4.5)
12
12
  nio4r (~> 2.0)
13
13
  websocket-driver (>= 0.6.1)
14
- actionmailer (5.2.4.4)
15
- actionpack (= 5.2.4.4)
16
- actionview (= 5.2.4.4)
17
- activejob (= 5.2.4.4)
14
+ actionmailer (5.2.4.5)
15
+ actionpack (= 5.2.4.5)
16
+ actionview (= 5.2.4.5)
17
+ activejob (= 5.2.4.5)
18
18
  mail (~> 2.5, >= 2.5.4)
19
19
  rails-dom-testing (~> 2.0)
20
- actionpack (5.2.4.4)
21
- actionview (= 5.2.4.4)
22
- activesupport (= 5.2.4.4)
20
+ actionpack (5.2.4.5)
21
+ actionview (= 5.2.4.5)
22
+ activesupport (= 5.2.4.5)
23
23
  rack (~> 2.0, >= 2.0.8)
24
24
  rack-test (>= 0.6.3)
25
25
  rails-dom-testing (~> 2.0)
26
26
  rails-html-sanitizer (~> 1.0, >= 1.0.2)
27
- actionview (5.2.4.4)
28
- activesupport (= 5.2.4.4)
27
+ actionview (5.2.4.5)
28
+ activesupport (= 5.2.4.5)
29
29
  builder (~> 3.1)
30
30
  erubi (~> 1.4)
31
31
  rails-dom-testing (~> 2.0)
32
32
  rails-html-sanitizer (~> 1.0, >= 1.0.3)
33
- activejob (5.2.4.4)
34
- activesupport (= 5.2.4.4)
33
+ activejob (5.2.4.5)
34
+ activesupport (= 5.2.4.5)
35
35
  globalid (>= 0.3.6)
36
- activemodel (5.2.4.4)
37
- activesupport (= 5.2.4.4)
38
- activerecord (5.2.4.4)
39
- activemodel (= 5.2.4.4)
40
- activesupport (= 5.2.4.4)
36
+ activemodel (5.2.4.5)
37
+ activesupport (= 5.2.4.5)
38
+ activerecord (5.2.4.5)
39
+ activemodel (= 5.2.4.5)
40
+ activesupport (= 5.2.4.5)
41
41
  arel (>= 9.0)
42
- activestorage (5.2.4.4)
43
- actionpack (= 5.2.4.4)
44
- activerecord (= 5.2.4.4)
42
+ activestorage (5.2.4.5)
43
+ actionpack (= 5.2.4.5)
44
+ activerecord (= 5.2.4.5)
45
45
  marcel (~> 0.3.1)
46
- activesupport (5.2.4.4)
46
+ activesupport (5.2.4.5)
47
47
  concurrent-ruby (~> 1.0, >= 1.0.2)
48
48
  i18n (>= 0.7, < 2)
49
49
  minitest (~> 5.1)
@@ -54,25 +54,25 @@ GEM
54
54
  thor (>= 0.14.0)
55
55
  arel (9.0.0)
56
56
  ast (2.4.1)
57
- bootsnap (1.5.1)
57
+ bootsnap (1.7.2)
58
58
  msgpack (~> 1.0)
59
59
  builder (3.2.4)
60
60
  byebug (11.1.3)
61
61
  climate_control (0.2.0)
62
62
  coderay (1.1.3)
63
- concurrent-ruby (1.1.7)
63
+ concurrent-ruby (1.1.8)
64
64
  crass (1.0.6)
65
65
  diff-lcs (1.4.4)
66
- erubi (1.9.0)
66
+ erubi (1.10.0)
67
67
  ffi (1.14.2)
68
68
  globalid (0.4.2)
69
69
  activesupport (>= 4.2.0)
70
- i18n (1.8.5)
70
+ i18n (1.8.9)
71
71
  concurrent-ruby (~> 1.0)
72
72
  listen (3.4.1)
73
73
  rb-fsevent (~> 0.10, >= 0.10.3)
74
74
  rb-inotify (~> 0.9, >= 0.9.10)
75
- loofah (2.7.0)
75
+ loofah (2.9.0)
76
76
  crass (~> 1.0.2)
77
77
  nokogiri (>= 1.5.9)
78
78
  mail (2.7.1)
@@ -82,12 +82,13 @@ GEM
82
82
  method_source (1.0.0)
83
83
  mimemagic (0.3.5)
84
84
  mini_mime (1.0.2)
85
- mini_portile2 (2.4.0)
86
- minitest (5.14.2)
87
- msgpack (1.3.3)
88
- nio4r (2.5.4)
89
- nokogiri (1.10.10)
90
- mini_portile2 (~> 2.4.0)
85
+ mini_portile2 (2.5.0)
86
+ minitest (5.14.3)
87
+ msgpack (1.4.2)
88
+ nio4r (2.5.5)
89
+ nokogiri (1.11.1)
90
+ mini_portile2 (~> 2.5.0)
91
+ racc (~> 1.4)
91
92
  parallel (1.19.2)
92
93
  parser (2.7.1.4)
93
94
  ast (~> 2.4.1)
@@ -97,35 +98,36 @@ GEM
97
98
  pry-byebug (3.9.0)
98
99
  byebug (~> 11.0)
99
100
  pry (~> 0.13.0)
101
+ racc (1.5.2)
100
102
  rack (2.2.3)
101
103
  rack-test (1.1.0)
102
104
  rack (>= 1.0, < 3)
103
- rails (5.2.4.4)
104
- actioncable (= 5.2.4.4)
105
- actionmailer (= 5.2.4.4)
106
- actionpack (= 5.2.4.4)
107
- actionview (= 5.2.4.4)
108
- activejob (= 5.2.4.4)
109
- activemodel (= 5.2.4.4)
110
- activerecord (= 5.2.4.4)
111
- activestorage (= 5.2.4.4)
112
- activesupport (= 5.2.4.4)
105
+ rails (5.2.4.5)
106
+ actioncable (= 5.2.4.5)
107
+ actionmailer (= 5.2.4.5)
108
+ actionpack (= 5.2.4.5)
109
+ actionview (= 5.2.4.5)
110
+ activejob (= 5.2.4.5)
111
+ activemodel (= 5.2.4.5)
112
+ activerecord (= 5.2.4.5)
113
+ activestorage (= 5.2.4.5)
114
+ activesupport (= 5.2.4.5)
113
115
  bundler (>= 1.3.0)
114
- railties (= 5.2.4.4)
116
+ railties (= 5.2.4.5)
115
117
  sprockets-rails (>= 2.0.0)
116
118
  rails-dom-testing (2.0.3)
117
119
  activesupport (>= 4.2.0)
118
120
  nokogiri (>= 1.6)
119
121
  rails-html-sanitizer (1.3.0)
120
122
  loofah (~> 2.3)
121
- railties (5.2.4.4)
122
- actionpack (= 5.2.4.4)
123
- activesupport (= 5.2.4.4)
123
+ railties (5.2.4.5)
124
+ actionpack (= 5.2.4.5)
125
+ activesupport (= 5.2.4.5)
124
126
  method_source
125
127
  rake (>= 0.8.7)
126
128
  thor (>= 0.19.0, < 2.0)
127
129
  rainbow (3.0.0)
128
- rake (13.0.1)
130
+ rake (13.0.3)
129
131
  rb-fsevent (0.10.4)
130
132
  rb-inotify (0.10.1)
131
133
  ffi (~> 1.0)
@@ -167,15 +169,15 @@ GEM
167
169
  activesupport (>= 4.0)
168
170
  sprockets (>= 3.0.0)
169
171
  sqlite3 (1.4.2)
170
- thor (1.0.1)
172
+ thor (1.1.0)
171
173
  thread_safe (0.3.6)
172
- tzinfo (1.2.7)
174
+ tzinfo (1.2.9)
173
175
  thread_safe (~> 0.1)
174
176
  unicode-display_width (1.7.0)
175
177
  websocket-driver (0.7.3)
176
178
  websocket-extensions (>= 0.1.0)
177
179
  websocket-extensions (0.1.5)
178
- yard (0.9.25)
180
+ yard (0.9.26)
179
181
 
180
182
  PLATFORMS
181
183
  ruby
@@ -28,13 +28,12 @@ module DeclareSchema
28
28
  field(:lock_version, :integer, default: 1, null: false)
29
29
  end
30
30
 
31
- def field(name, type, *args)
32
- options = args.extract_options!
33
- @model.declare_field(name, type, *(args + [@options.merge(options)]))
31
+ def field(name, type, *args, **options)
32
+ @model.declare_field(name, type, *[*args, @options.merge(options)])
34
33
  end
35
34
 
36
35
  def method_missing(name, *args)
37
- field(name, args.first, *args[1..-1])
36
+ field(name, *args)
38
37
  end
39
38
  end
40
39
  end
@@ -81,8 +81,7 @@ module DeclareSchema
81
81
  # arguments. The arguments are forwarded to the #field_added
82
82
  # callback, allowing custom metadata to be added to field
83
83
  # declarations.
84
- def declare_field(name, type, *args)
85
- options = args.extract_options!
84
+ def declare_field(name, type, *args, **options)
86
85
  try(:field_added, name, type, args, options)
87
86
  add_serialize_for_field(name, type, options)
88
87
  add_formatting_for_field(name, type)
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ class UnknownTypeError < 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.empty? || native_types[type]) # empty will happen with NullDBAdapter used in assets:precompile
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 deserialize_default_value(column, type, default_value)
52
+ type or raise ArgumentError, "must pass type; got #{type.inspect}"
53
+
54
+ case Rails::VERSION::MAJOR
55
+ when 4
56
+ # TODO: Delete this Rails 4 support ASAP! This could be wrong, since it's using the type of the old column...which
57
+ # might be getting migrated to a new type. We should be using just type as below. -Colin
58
+ column.type_cast_from_database(default_value)
59
+ else
60
+ cast_type = ActiveRecord::Base.connection.send(:lookup_cast_type, type) or
61
+ raise "cast_type not found for #{type}"
62
+ cast_type.deserialize(default_value)
63
+ end
64
+ end
65
+
66
+ # Normalizes schema attributes for the given database adapter name.
67
+ # Note that the un-normalized attributes are still useful for generating migrations because those
68
+ # may be run with a different adapter.
69
+ # This method never mutates its argument.
70
+ def normalize_schema_attributes(schema_attributes, db_adapter_name)
71
+ case schema_attributes[:type]
72
+ when :boolean
73
+ schema_attributes.reverse_merge(limit: 1)
74
+ when :integer
75
+ schema_attributes.reverse_merge(limit: 8) if db_adapter_name.match?(/sqlite/i)
76
+ when :float
77
+ schema_attributes.except(:limit)
78
+ when :text
79
+ schema_attributes.except(:limit) if db_adapter_name.match?(/sqlite/i)
80
+ when :datetime
81
+ schema_attributes.reverse_merge(precision: 0)
82
+ when NilClass
83
+ raise ArgumentError, ":type key not found; keys: #{schema_attributes.keys.inspect}"
84
+ end || schema_attributes
85
+ end
86
+
87
+ def equivalent_schema_attributes?(schema_attributes_lhs, schema_attributes_rhs)
88
+ db_adapter_name = ActiveRecord::Base.connection.class.name
89
+ normalized_lhs = normalize_schema_attributes(schema_attributes_lhs, db_adapter_name)
90
+ normalized_rhs = normalize_schema_attributes(schema_attributes_rhs, db_adapter_name)
91
+
92
+ normalized_lhs == normalized_rhs
93
+ end
94
+ end
95
+
96
+ attr_reader :type
97
+
98
+ def initialize(model, current_table_name, column)
99
+ @model = model or raise ArgumentError, "must pass model"
100
+ @current_table_name = current_table_name or raise ArgumentError, "must pass current_table_name"
101
+ @column = column or raise ArgumentError, "must pass column"
102
+ @type = @column.type
103
+ self.class.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect}"
104
+ end
105
+
106
+ SCHEMA_KEYS = [:type, :limit, :precision, :scale, :null, :default].freeze
107
+
108
+ # omits keys with nil values
109
+ def schema_attributes
110
+ SCHEMA_KEYS.each_with_object({}) do |key, result|
111
+ value =
112
+ case key
113
+ when :default
114
+ self.class.deserialize_default_value(@column, @type, @column.default)
115
+ else
116
+ col_value = @column.send(key)
117
+ if col_value.nil? && (native_type = self.class.native_types[@column.type])
118
+ native_type[key]
119
+ else
120
+ col_value
121
+ end
122
+ end
123
+
124
+ result[key] = value unless value.nil?
125
+ end.tap do |result|
126
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i) && @column.type.in?([:string, :text])
127
+ result.merge!(collation_and_charset_for_column(@current_table_name, @column.name))
128
+ end
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def collation_and_charset_for_column(current_table_name, column_name)
135
+ connection = ActiveRecord::Base.connection
136
+ connection.class.name.match?(/mysql/i) or raise ArgumentError, "only supported for MySQL"
137
+
138
+ database_name = connection.current_database
139
+
140
+ defaults = connection.select_one(<<~EOS)
141
+ SELECT C.character_set_name, C.collation_name
142
+ FROM information_schema.`COLUMNS` C
143
+ WHERE C.table_schema = '#{connection.quote_string(database_name)}' AND
144
+ C.table_name = '#{connection.quote_string(current_table_name)}' AND
145
+ C.column_name = '#{connection.quote_string(column_name)}';
146
+ EOS
147
+
148
+ defaults && defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect} from #{database_name}.#{current_table_name}.#{column_name}"
149
+ defaults && defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
150
+
151
+ {
152
+ charset: defaults["character_set_name"],
153
+ collation: defaults["collation_name"]
154
+ }
155
+ end
156
+ end
157
+ end
158
+ 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
@@ -30,7 +33,18 @@ module DeclareSchema
30
33
  end
31
34
  end
32
35
 
33
- attr_reader :model, :name, :type, :position, :options
36
+ attr_reader :model, :name, :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
34
48
 
35
49
  def initialize(model, name, type, position: 0, **options)
36
50
  # TODO: TECH-5116
@@ -42,170 +56,71 @@ module DeclareSchema
42
56
  @model = model
43
57
  @name = name.to_sym
44
58
  type.is_a?(Symbol) or raise ArgumentError, "type must be a Symbol; got #{type.inspect}"
45
- @type = type
59
+ @type = TYPE_SYNONYMS[type] || type
46
60
  @position = position
47
- @options = options
48
- case type
61
+ @options = options.dup
62
+
63
+ @options.has_key?(:null) or @options[:null] = false
64
+
65
+ case @type
49
66
  when :text
50
- @options[:default] and raise "default may not be given for :text field #{model}##{@name}"
51
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}"
52
69
  @options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit] || MYSQL_LONGTEXT_LIMIT)
70
+ else
71
+ @options.delete(:limit)
53
72
  end
54
73
  when :string
55
- @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`?"
56
75
  when :bigint
57
76
  @type = :integer
58
- @options = options.merge(limit: 8)
77
+ @options[:limit] = 8
59
78
  end
60
79
 
61
- if type.in?([:text, :string])
62
- if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
63
- @options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
64
- @options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
65
- end
80
+ Column.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect} not found in #{Column.native_types.inspect} for adapter #{ActiveRecord::Base.connection.class.name}"
81
+
82
+ if @type.in?([:string, :text, :binary, :varbinary, :integer, :enum])
83
+ @options[:limit] ||= Column.native_types[@type][:limit]
66
84
  else
67
- @options[:charset] and raise "charset may only given for :string and :text fields"
68
- @options[:collation] and raise "collation may only given for :string and :text fields"
85
+ @type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@type} in field #{model}##{@name}")
86
+ @options.delete(:limit)
69
87
  end
70
- end
71
88
 
72
- TYPE_SYNONYMS = { timestamp: :datetime }.freeze
73
-
74
- SQLITE_COLUMN_CLASS =
75
- begin
76
- ActiveRecord::ConnectionAdapters::SQLiteColumn
77
- rescue NameError
78
- NilClass
89
+ if @type == :decimal
90
+ @options[:precision] or warn("precision: required for :decimal type in field #{model}##{@name}")
91
+ @options[:scale] or warn("scale: required for :decimal type in field #{model}##{@name}")
92
+ else
93
+ if @type != :datetime
94
+ @options.has_key?(:precision) and warn("precision: only allowed for :decimal type or :datetime for SQL type #{@type} in field #{model}##{@name}")
95
+ end
96
+ @options.has_key?(:scale) and warn("scale: only allowed for :decimal type for SQL type #{@type} in field #{model}##{@name}")
79
97
  end
80
98
 
81
- def sql_type
82
- @options[:sql_type] || begin
83
- if native_type?(type)
84
- type
85
- else
86
- field_class = DeclareSchema.to_class(type)
87
- field_class && field_class::COLUMN_TYPE or raise UnknownSqlTypeError, "#{type.inspect} for #{model}##{@name}"
88
- end
89
- end
90
- end
91
-
92
- def sql_options
93
- @options.except(:ruby_default, :validates)
94
- end
95
-
96
- def limit
97
- @options[:limit] || native_types[sql_type][:limit]
98
- end
99
-
100
- def precision
101
- @options[:precision]
102
- end
103
-
104
- def scale
105
- @options[:scale]
106
- end
107
-
108
- def null
109
- !:null.in?(@options) || @options[:null]
110
- end
111
-
112
- def default
113
- @options[:default]
114
- end
115
-
116
- def charset
117
- @options[:charset]
118
- end
119
-
120
- def collation
121
- @options[:collation]
122
- end
123
-
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
99
+ if @type.in?([:text, :string])
100
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
101
+ @options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
102
+ @options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
159
103
  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)
104
+ @options.delete(:charset)
105
+ @options.delete(:collation)
165
106
  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
107
  else
199
- {}
108
+ @options[:charset] and warn("charset may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
109
+ @options[:collation] and warne("collation may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
200
110
  end
201
- end
202
111
 
203
- def native_type?(type)
204
- type.to_sym != :primary_key && native_types.has_key?(type)
112
+ @options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]
113
+
114
+ @sql_options = @options.slice(*SQL_OPTIONS)
205
115
  end
206
116
 
207
- def native_types
208
- Generators::DeclareSchema::Migration::Migrator.native_types
117
+ # returns the attributes for schema migrations as a Hash
118
+ # omits name and position since those are meta-data above the schema
119
+ # omits keys with nil values
120
+ def schema_attributes(col_spec)
121
+ @sql_options.merge(type: @type).tap do |attrs|
122
+ attrs[:default] = Column.deserialize_default_value(col_spec, @type, attrs[:default])
123
+ end.compact
209
124
  end
210
125
  end
211
126
  end