declare_schema 0.6.2 → 0.8.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d4a25e64860b280537a5c131f10537323b6908099a8c58fd72c8da6e8117556
4
- data.tar.gz: 9b376403b635880e2e7bf3a8cd46dbd1bedc2dec030f7be03dc7a671ca956128
3
+ metadata.gz: 63c52c12c292be619b08901eb088c9957e8bfbc9708d087faa9d418704524175
4
+ data.tar.gz: 5a12e3d6cc67914e6b8c8739c17d89e2ea7886212f2abd90bd372f57fab808af
5
5
  SHA512:
6
- metadata.gz: 3b8709130944d5eef82e8960a40df117b50c84d29b5ff1ba222bab5f8ff3978a84d2971b022b83c2bd122c2681aa715bd65e67238bc231eb1494d06949e0a297
7
- data.tar.gz: bcf79bbe85dfef64f34a9d56c8f3cb7817a89796ddb086b3ddced1b7e91b1d79fe80f0f1f89de759c81417691cf3315c21655fbd4b93a3ced32495f601c1562f
6
+ metadata.gz: e33ae65cdfcb5fc56c24b4d62804ba28c77b29efc0a7659b45660c953f6310ae90b8b42a8d51f9f5331592e511ec33d56ff0f7b691dcb7aa8f299a5f7d523f22
7
+ data.tar.gz: 688ca01165e655ab6ff309f82ba336530bb44d695ac90de049cabc730850c7059c8789ef95bafbc02059b6329b4212d0e2ab726034b9c71d814a51fdaca02501
data/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ 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.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
24
+ - Fixed a bug where the generated call to add_foreign_key() was not setting `column:`,
25
+ so it only worked in cases where Rails could infer the foreign key by convention.
26
+
27
+ ## [0.6.3] - 2021-01-21
28
+ ### Added
29
+ - Added `add_foreign_key` native rails call in `DeclareSchema::Model::ForeignKeyDefinition#to_add_statement`.
30
+
31
+ ### Fixed
32
+ - Fixed a bug in migration generation caused by `DeclareSchema::Migration#create_constraints`
33
+ calling `DeclareSchema::Model::ForeignKeyDefinition#to_add_statement` with unused parameters.
34
+
35
+ - Fixed a bug in `DeclareSchema::Migration#remove_foreign_key` where special characters would not be quoted properly.
36
+
7
37
  ## [0.6.2] - 2021-01-06
8
38
  ### Added
9
39
  - Added `sqlite3` as dev dependency for local development
@@ -100,6 +130,11 @@ using the appropriate Rails configuration attributes.
100
130
  ### Added
101
131
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
102
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
136
+ [0.6.4]: https://github.com/Invoca/declare_schema/compare/v0.6.3...v0.6.4
137
+ [0.6.3]: https://github.com/Invoca/declare_schema/compare/v0.6.2...v0.6.3
103
138
  [0.6.2]: https://github.com/Invoca/declare_schema/compare/v0.6.1...v0.6.2
104
139
  [0.6.1]: https://github.com/Invoca/declare_schema/compare/v0.6.0...v0.6.1
105
140
  [0.6.0]: https://github.com/Invoca/declare_schema/compare/v0.5.0...v0.6.0
data/Gemfile.lock CHANGED
@@ -1,49 +1,49 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.6.2)
4
+ declare_schema (0.8.0.pre.1)
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
- listen (3.4.0)
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[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 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}"
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