declare_schema 0.4.2 → 0.5.0.pre.1

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: bf6d8bf1670910eeadc3f7ed08860af6630f1b5edc271a0740fe8bb94e7f5c71
4
- data.tar.gz: 4d058e02cb6b269f6b66fa20f8ac661ecdb7f8b31547cf43e65e27c869221902
3
+ metadata.gz: 3f3b9aed36bcc541fb7d929a97e3650358be4da05c33a5d27e8db732633eb8f8
4
+ data.tar.gz: 800c945108be2e29301afc2badce461a1302dadad7c393bd39c939368311e874
5
5
  SHA512:
6
- metadata.gz: d95d741db5f9279cfe39f7bd972581708a0ccae7e2d9d209318282a9980d2980e3611592ba08b6b53136e23b92ac4aaa51e2a66187002de04bf818c29715820f
7
- data.tar.gz: 93c203bb2ff2255f014d76a3069d1530d97f9603293738844e2faf6f2c9cb6fb95dcc2aa9ad505df15c8e1e14567e076a5e2901b7b4847f804ce3dd85dc5b313
6
+ metadata.gz: 821e56fe2ba4e7c9913f630a47cf685a565c19d40fdf282305c3bf0db6f0783b3174aa4e1ca535732e64cc7514cd87a598eaf743fbca0b21fa7041ec62ce82a0
7
+ data.tar.gz: c2522286a389e559fda6b635b0d85b1025bdf918a98a5eb27e40946d5eee1e079efca53a0e2ad6f24649a8b05f59d652869cee924dcca7491ff7575d8292519c
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ day: friday
8
+ time: "22:00"
9
+ timezone: PST8PDT
10
+ open-pull-requests-limit: 99
11
+ versioning-strategy: lockfile-only
12
+ commit-message:
13
+ prefix: No-Jira
14
+ include: scope
@@ -4,6 +4,11 @@ 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.5.0] - Unreleased
8
+ ### Added
9
+ - Added support for configuring the character set and collation for MySQL databases
10
+ at the global, table, and field level
11
+
7
12
  ## [0.4.2] - 2020-12-05
8
13
  ### Fixed
9
14
  - Generalize the fix below to sqlite || Rails 4.
@@ -15,7 +20,7 @@ Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0
15
20
  ## [0.4.0] - 2020-11-20
16
21
  ### Added
17
22
  - Fields may be declared with `serialize: true` (any value with a valid `.to_yaml` stored as YAML),
18
- or `serialize: <serializeable-class>`, where `<serializeable-class>`
23
+ or `serialize: <serializeable-class>`, where `<serializeable-class>`
19
24
  may be `Array` (`Array` stored as YAML) or `Hash` (`Hash` stored as YAML) or `JSON` (any value with a valid `.to_json`, stored as JSON)
20
25
  or any custom serializable class.
21
26
  This invokes `ActiveSupport`'s `serialize` macro for that field, passing the serializable class, if given.
@@ -68,6 +73,7 @@ using the appropriate Rails configuration attributes.
68
73
  ### Added
69
74
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
70
75
 
76
+ [0.5.0]: https://github.com/Invoca/declare_schema/compare/v0.4.2...v0.5.0
71
77
  [0.4.2]: https://github.com/Invoca/declare_schema/compare/v0.4.1...v0.4.2
72
78
  [0.4.1]: https://github.com/Invoca/declare_schema/compare/v0.4.0...v0.4.1
73
79
  [0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
data/Gemfile CHANGED
@@ -14,6 +14,7 @@ gem 'bundler', '< 2'
14
14
  gem "climate_control", '~> 0.2'
15
15
  gem 'pry'
16
16
  gem 'pry-byebug'
17
+ gem 'mysql2'
17
18
  gem 'rails', '~> 5.2', '>= 5.2.4.3'
18
19
  gem 'responders'
19
20
  gem 'rspec'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.4.2)
4
+ declare_schema (0.5.0.pre.1)
5
5
  rails (>= 4.2)
6
6
 
7
7
  GEM
@@ -85,6 +85,7 @@ GEM
85
85
  mini_portile2 (2.4.0)
86
86
  minitest (5.14.2)
87
87
  msgpack (1.3.3)
88
+ mysql2 (0.5.3)
88
89
  nio4r (2.5.4)
89
90
  nokogiri (1.10.10)
90
91
  mini_portile2 (~> 2.4.0)
@@ -187,6 +188,7 @@ DEPENDENCIES
187
188
  climate_control (~> 0.2)
188
189
  declare_schema!
189
190
  listen
191
+ mysql2
190
192
  pry
191
193
  pry-byebug
192
194
  rails (~> 5.2, >= 5.2.4.3)
data/README.md CHANGED
@@ -70,6 +70,72 @@ DeclareSchema::Migration::Migrator.before_generating_migration do
70
70
  end
71
71
  ```
72
72
 
73
+ ## Declaring Character Set and Collation
74
+ _Note: This feature currently only works for MySQL database configurations._
75
+
76
+ MySQL originally supported UTF-8 in the range of 1-3 bytes (`mb3` or "multi-byte 3")
77
+ which covered the full set of Unicode code points at the time: U+0000 - U+FFFF.
78
+ But later, Unicode was extended beyond U+FFFF to make room for emojis, and with that
79
+ UTF-8 require 1-4 bytes (`mb4` or "multi-byte 4"). With this addition, there has
80
+ come a need to dynamically define the character set and collation for individual
81
+ tables and columns in the database. With `declare_schema` this can be configured
82
+ at three separate levels
83
+
84
+ ### Global Configuration
85
+ The character set and collation for all tables and fields can be set at the global level
86
+ using the `Generators::DeclareSchema::Migrator.default_charset=` and
87
+ `Generators::DeclareSchema::Migrator.default_collation=` configuration methods.
88
+
89
+ For example, adding the following to your `config/initializers` directory will
90
+ turn all tables into `utf8mb4` supporting tables:
91
+
92
+ **declare_schema.rb**
93
+ ```ruby
94
+ # frozen_string_literal: true
95
+
96
+ Generators::DeclareSchema::Migrator.default_charset = "utf8mb4"
97
+ Generators::DeclareSchema::Migrator.default_collation = "utf8mb4_general"
98
+ ```
99
+
100
+ ### Table Configuration
101
+ In order to configure a table's default character set and collation, the `charset` and
102
+ `collation` arguments can be added to the `fields` block.
103
+
104
+ For example, if you have a comments model that needs `utf8mb4` support, it would look
105
+ like the following:
106
+
107
+ **app/models/comment.rb**
108
+ ```ruby
109
+ # frozen_string_literal: true
110
+
111
+ class Comment < ActiveRecord::Base
112
+ fields charset: "utf8mb4", collation: "utf8mb4_general" do
113
+ subject :string, limit: 255
114
+ content :text, limit: 0xffff_ffff
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### Field Configuration
120
+ If you're looking to only change the character set and collation for a single field
121
+ in the table, simply set the `charset` and `collation` configuration options on the
122
+ field definition itself.
123
+
124
+ For example, if you only want to support `utf8mb4` for the content of a comment, it would
125
+ look like the following:
126
+
127
+ **app/models/comment.rb**
128
+ ```ruby
129
+ # frozen_string_literal: true
130
+
131
+ class Comment < ActiveRecord::Base
132
+ fields do
133
+ subject :string, limit: 255
134
+ context :text, limit: 0xffff_ffff, charset: "utf8mb4", collation: "utf8mb4_general"
135
+ end
136
+ end
137
+ ```
138
+
73
139
  ## Installing
74
140
 
75
141
  Install the `DeclareSchema` gem directly:
@@ -7,6 +7,7 @@ gem "bundler", "< 2"
7
7
  gem "climate_control", "~> 0.2"
8
8
  gem "pry"
9
9
  gem "pry-byebug"
10
+ gem "mysql2"
10
11
  gem "rails", "~> 4.2"
11
12
  gem "responders"
12
13
  gem "rspec"
@@ -7,6 +7,7 @@ gem "bundler", "< 2"
7
7
  gem "climate_control", "~> 0.2"
8
8
  gem "pry"
9
9
  gem "pry-byebug"
10
+ gem "mysql2"
10
11
  gem "rails", "~> 5.2"
11
12
  gem "responders"
12
13
  gem "rspec"
@@ -7,6 +7,7 @@ gem "bundler", "< 2"
7
7
  gem "climate_control", "~> 0.2"
8
8
  gem "pry"
9
9
  gem "pry-byebug"
10
+ gem "mysql2"
10
11
  gem "rails", "~> 6.0"
11
12
  gem "responders"
12
13
  gem "rspec"
@@ -41,5 +41,6 @@ require 'declare_schema/model'
41
41
  require 'declare_schema/model/field_spec'
42
42
  require 'declare_schema/model/index_definition'
43
43
  require 'declare_schema/model/foreign_key_definition'
44
+ require 'declare_schema/model/table_options_definition'
44
45
 
45
46
  require 'declare_schema/railtie' if defined?(Rails)
@@ -6,12 +6,13 @@ require 'declare_schema/field_declaration_dsl'
6
6
 
7
7
  module DeclareSchema
8
8
  module FieldsDsl
9
- def fields(&block)
9
+ def fields(table_options = {}, &block)
10
10
  # Any model that calls 'fields' gets DeclareSchema::Model behavior
11
11
  DeclareSchema::Model.mix_in(self)
12
12
 
13
13
  # @include_in_migration = false #||= options.fetch(:include_in_migration, true); options.delete(:include_in_migration)
14
14
  @include_in_migration = true
15
+ @table_options = table_options
15
16
 
16
17
  if block
17
18
  dsl = DeclareSchema::FieldDeclarationDsl.new(self, null: false)
@@ -30,6 +30,10 @@ module DeclareSchema
30
30
  inheriting_cattr_reader ignore_indexes: []
31
31
  inheriting_cattr_reader constraint_specs: []
32
32
 
33
+ # table_options holds optional configuration for the create_table statement
34
+ # supported options include :charset and :collation
35
+ inheriting_cattr_reader table_options: HashWithIndifferentAccess.new
36
+
33
37
  # eval avoids the ruby 1.9.2 "super from singleton method ..." error
34
38
 
35
39
  eval %(
@@ -54,6 +54,9 @@ module DeclareSchema
54
54
  end
55
55
  when :string
56
56
  @options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
57
+ else
58
+ @options[:collation] and raise "collation may only given for :string and :text fields"
59
+ @options[:charset] and raise "charset may only given for :string and :text fields"
57
60
  end
58
61
  @position = position_option || model.field_specs.length
59
62
  end
@@ -102,6 +105,18 @@ module DeclareSchema
102
105
  @options[:default]
103
106
  end
104
107
 
108
+ def collation
109
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
110
+ (@options[:collation] || model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation).to_s
111
+ end
112
+ end
113
+
114
+ def charset
115
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
116
+ (@options[:charset] || model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset).to_s
117
+ end
118
+ end
119
+
105
120
  def same_type?(col_spec)
106
121
  type = sql_type
107
122
  normalized_type = TYPE_SYNONYMS[type] || type
@@ -109,36 +124,77 @@ module DeclareSchema
109
124
  normalized_type == normalized_col_spec_type
110
125
  end
111
126
 
112
- def different_to?(col_spec)
113
- !same_type?(col_spec) ||
114
- begin
115
- native_type = native_types[type]
116
- check_attributes = [:null, :default]
117
- 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
118
- check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
119
- check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
120
- (sql_type == :text && self.class.mysql_text_limits?)
121
- check_attributes.any? do |k|
122
- if k == :default
123
- case Rails::VERSION::MAJOR
124
- when 4
125
- col_spec.type_cast_from_database(col_spec.default) != col_spec.type_cast_from_database(default)
126
- else
127
- cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
128
- cast_type.deserialize(col_spec.default) != cast_type.deserialize(default)
129
- end
130
- else
131
- col_value = col_spec.send(k)
132
- if col_value.nil? && native_type
133
- col_value = native_type[k]
134
- end
135
- col_value != send(k)
136
- end
127
+ def different_to?(table_name, col_spec)
128
+ !same_as(table_name, col_spec)
129
+ end
130
+
131
+ def same_as(table_name, col_spec)
132
+ same_type?(col_spec) &&
133
+ same_attributes?(col_spec) &&
134
+ (!type.in?([:text, :string]) || same_charset_and_collation?(table_name, col_spec))
135
+ end
136
+
137
+ private
138
+
139
+ def same_attributes?(col_spec)
140
+ native_type = native_types[type]
141
+ check_attributes = [:null, :default]
142
+ 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
143
+ check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
144
+ check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
145
+ (sql_type == :text && self.class.mysql_text_limits?)
146
+ check_attributes.all? do |k|
147
+ if k == :default
148
+ case Rails::VERSION::MAJOR
149
+ when 4
150
+ col_spec.type_cast_from_database(col_spec.default) == col_spec.type_cast_from_database(default)
151
+ else
152
+ cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
153
+ cast_type.deserialize(col_spec.default) == cast_type.deserialize(default)
154
+ end
155
+ else
156
+ col_value = col_spec.send(k)
157
+ if col_value.nil? && native_type
158
+ col_value = native_type[k]
137
159
  end
160
+ col_value == send(k)
138
161
  end
162
+ end
139
163
  end
140
164
 
141
- private
165
+ def same_charset_and_collation?(table_name, col_spec)
166
+ current_collation_and_charset = collation_and_charset_for_column(table_name, col_spec)
167
+
168
+ collation == current_collation_and_charset[:collation] &&
169
+ charset == current_collation_and_charset[:charset]
170
+ end
171
+
172
+ def collation_and_charset_for_column(table_name, col_spec)
173
+ column_name = col_spec.name
174
+ connection = ActiveRecord::Base.connection
175
+
176
+ if connection.class.name.match?(/mysql/i)
177
+ database_name = connection.current_database
178
+
179
+ defaults = connection.select_one(<<~EOS)
180
+ SELECT C.character_set_name, C.collation_name
181
+ FROM information_schema.`COLUMNS` C
182
+ WHERE C.table_schema = #{connection.quote_string(database_name)} AND
183
+ C.table_name = #{connection.quote_string(table_name)} AND
184
+ C.column_name = #{connection.quote_string(column_name)};
185
+ EOS
186
+
187
+ defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
188
+ defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
189
+
190
+ {
191
+ charset: defaults["character_set_name"],
192
+ collation: defaults["collation_name"]
193
+ }
194
+ else
195
+ {}
196
+ end
197
+ end
142
198
 
143
199
  def native_type?(type)
144
200
  type.to_sym != :primary_key && native_types.has_key?(type)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class TableOptionsDefinition
6
+ include Comparable
7
+
8
+ TABLE_OPTIONS_TO_SQL_MAPPINGS = {
9
+ charset: 'CHARACTER SET',
10
+ collation: 'COLLATE'
11
+ }.freeze
12
+
13
+ class << self
14
+ def for_model(model, old_table_name = nil)
15
+ table_name = old_table_name || model.table_name
16
+ table_options = if model.connection.class.name.match?(/mysql/i)
17
+ mysql_table_options(model.connection, table_name)
18
+ else
19
+ {}
20
+ end
21
+
22
+ new(table_name, table_options)
23
+ end
24
+
25
+ private
26
+
27
+ def mysql_table_options(connection, table_name)
28
+ database = connection.current_database
29
+ defaults = connection.select_one(<<~EOS)
30
+ SELECT CCSA.character_set_name, CCSA.collation_name
31
+ FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
32
+ WHERE CCSA.collation_name = T.table_collation AND
33
+ T.table_schema = #{connection.quote_string(database)} AND
34
+ T.table_name = #{connection.quote_string(table_name)};
35
+ EOS
36
+
37
+ defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
38
+ defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
39
+
40
+ {
41
+ charset: defaults["character_set_name"],
42
+ collation: defaults["collation_name"]
43
+ }
44
+ end
45
+ end
46
+
47
+ attr_reader :table_name, :table_options
48
+
49
+ def initialize(table_name, table_options = {})
50
+ @table_name = table_name
51
+ @table_options = table_options
52
+ end
53
+
54
+ def to_key
55
+ @key ||= [table_name, table_options].map(&:to_s)
56
+ end
57
+
58
+ def settings
59
+ @settings ||= table_options.map { |name, value| "#{TABLE_OPTIONS_TO_SQL_MAPPINGS[name]} #{value}" if value }.compact.join(" ")
60
+ end
61
+
62
+ def hash
63
+ to_key.hash
64
+ end
65
+
66
+ def <=>(rhs)
67
+ to_key <=> rhs.to_key
68
+ end
69
+
70
+ def equivalent?(rhs)
71
+ settings == rhs.settings
72
+ end
73
+
74
+ alias eql? ==
75
+ alias to_s settings
76
+
77
+ def alter_table_statement
78
+ statement = "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name)} #{to_s};"
79
+ "execute #{statement.inspect}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.0.pre.1"
5
5
  end
@@ -69,13 +69,19 @@ module Generators
69
69
  class Migrator
70
70
  class Error < RuntimeError; end
71
71
 
72
- @ignore_models = []
73
- @ignore_tables = []
72
+ DEFAULT_CHARSET = :utf8mb4
73
+ DEFAULT_COLLATION = :utf8mb4_general
74
+
75
+ @ignore_models = []
76
+ @ignore_tables = []
74
77
  @before_generating_migration_callback = nil
75
- @active_record_class = ActiveRecord::Base
78
+ @active_record_class = ActiveRecord::Base
79
+ @default_charset = DEFAULT_CHARSET
80
+ @default_collation = DEFAULT_COLLATION
76
81
 
77
82
  class << self
78
- attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints, :active_record_class
83
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints,
84
+ :active_record_class, :default_charset, :default_collation
79
85
  attr_reader :before_generating_migration_callback
80
86
 
81
87
  def active_record_class
@@ -292,52 +298,76 @@ module Generators
292
298
  "drop_table :#{t}"
293
299
  end * "\n"
294
300
 
295
- changes = []
296
- undo_changes = []
297
- index_changes = []
298
- undo_index_changes = []
299
- fk_changes = []
300
- undo_fk_changes = []
301
+ changes = []
302
+ undo_changes = []
303
+ index_changes = []
304
+ undo_index_changes = []
305
+ fk_changes = []
306
+ undo_fk_changes = []
307
+ table_options_changes = []
308
+ undo_table_options_changes = []
309
+
301
310
  to_change.each do |t|
302
311
  model = models_by_table_name[t]
303
312
  table = to_rename.key(t) || model.table_name
304
313
  if table.in?(db_tables)
305
- change, undo, index_change, undo_index, fk_change, undo_fk = change_table(model, table)
314
+ change, undo, index_change, undo_index, fk_change, undo_fk, table_options_change, undo_table_options_change = change_table(model, table)
306
315
  changes << change
307
316
  undo_changes << undo
308
317
  index_changes << index_change
309
318
  undo_index_changes << undo_index
310
319
  fk_changes << fk_change
311
320
  undo_fk_changes << undo_fk
321
+ table_options_changes << table_options_change
322
+ undo_table_options_changes << undo_table_options_change
312
323
  end
313
324
  end
314
325
 
315
- up = [renames, drops, creates, changes, index_changes, fk_changes].flatten.reject(&:blank?) * "\n\n"
316
- down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes].flatten.reject(&:blank?) * "\n\n"
326
+ up = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten.reject(&:blank?) * "\n\n"
327
+ down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes, undo_table_options_changes].flatten.reject(&:blank?) * "\n\n"
317
328
 
318
329
  [up, down]
319
330
  end
320
331
 
321
332
  def create_table(model)
322
- longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
323
- disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
324
- primary_key_option =
325
- if model.primary_key.blank? || disable_auto_increment
326
- ", id: false"
327
- elsif model.primary_key == "id"
328
- ", id: :bigint"
329
- else
330
- ", primary_key: :#{model.primary_key}"
333
+ longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
334
+ disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
335
+ table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
336
+ field_definitions = [
337
+ disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
338
+ *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
339
+ ].compact
340
+
341
+ <<~EOS.strip
342
+ create_table :#{model.table_name}, #{create_table_options(model, disable_auto_increment)} do |t|
343
+ #{field_definitions.join("\n")}
331
344
  end
332
- (["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
333
- [(disable_auto_increment ? " t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil)] +
334
- model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) } +
335
- ["end"] + (if Migrator.disable_indexing
336
- []
337
- else
338
- create_indexes(model) +
339
- create_constraints(model)
340
- end)).compact * "\n"
345
+
346
+ #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
347
+ #{create_indexes(model).join("\n") unless Migrator.disable_indexing}
348
+ #{create_constraints(model).join("\n") unless Migrator.disable_indexing}
349
+ EOS
350
+ end
351
+
352
+ def create_table_options(model, disable_auto_increment)
353
+ if model.primary_key.blank? || disable_auto_increment
354
+ "id: false"
355
+ elsif model.primary_key == "id"
356
+ "id: :bigint"
357
+ else
358
+ "primary_key: :#{model.primary_key}"
359
+ end
360
+ end
361
+
362
+ def table_options_for_model(model)
363
+ if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
364
+ {}
365
+ else
366
+ {
367
+ charset: model.table_options[:charset] || Migrator.default_charset,
368
+ collation: model.table_options[:collation] || Migrator.default_collation
369
+ }
370
+ end
341
371
  end
342
372
 
343
373
  def create_indexes(model)
@@ -351,7 +381,7 @@ module Generators
351
381
  def create_field(field_spec, field_name_width)
352
382
  options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
353
383
  args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
354
- format(" t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
384
+ format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
355
385
  end
356
386
 
357
387
  def change_table(model, current_table_name)
@@ -413,15 +443,17 @@ module Generators
413
443
  col_name = old_names[c] || c
414
444
  col = db_columns[col_name]
415
445
  spec = model.field_specs[c]
416
- if spec.different_to?(col) # TODO: TECH-4814 DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
446
+ if spec.different_to?(current_table_name, col) # TODO: TECH-4814 DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
417
447
  change_spec = fk_field_options(model, c)
418
448
  change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
419
449
  ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
420
450
  (spec.limit || col.limit)
421
- change_spec[:precision] = spec.precision unless spec.precision.nil?
422
- change_spec[:scale] = spec.scale unless spec.scale.nil?
423
- change_spec[:null] = spec.null unless spec.null && col.null
424
- change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
451
+ change_spec[:precision] = spec.precision unless spec.precision.nil?
452
+ change_spec[:scale] = spec.scale unless spec.scale.nil?
453
+ change_spec[:null] = spec.null unless spec.null && col.null
454
+ change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
455
+ change_spec[:collation] = spec.collation unless spec.collation.nil?
456
+ change_spec[:charset] = spec.charset unless spec.charset.nil?
425
457
 
426
458
  changes << "change_column :#{new_table_name}, :#{c}, " +
427
459
  ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
@@ -436,13 +468,20 @@ module Generators
436
468
  else
437
469
  change_foreign_key_constraints(model, current_table_name)
438
470
  end
471
+ table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
472
+ change_table_options(model, current_table_name)
473
+ else
474
+ [[], []]
475
+ end
439
476
 
440
477
  [(renames + adds + removes + changes) * "\n",
441
478
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
442
479
  index_changes * "\n",
443
480
  undo_index_changes * "\n",
444
481
  fk_changes * "\n",
445
- undo_fk_changes * "\n"]
482
+ undo_fk_changes * "\n",
483
+ table_options_changes * "\n",
484
+ undo_table_options_changes * "\n"]
446
485
  end
447
486
 
448
487
  def change_indexes(model, old_table_name)
@@ -552,6 +591,20 @@ module Generators
552
591
  end
553
592
  end
554
593
 
594
+ def change_table_options(model, current_table_name)
595
+ old_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.for_model(model, current_table_name)
596
+ new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
597
+
598
+ if old_options_definition.equivalent?(new_options_definition)
599
+ [[], []]
600
+ else
601
+ [
602
+ [new_options_definition.alter_table_statement],
603
+ [old_options_definition.alter_table_statement]
604
+ ]
605
+ end
606
+ end
607
+
555
608
  def revert_table(table)
556
609
  res = StringIO.new
557
610
  schema_dumper_klass = case Rails::VERSION::MAJOR
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/mysql2_adapter'
4
+ require_relative '../../../../lib/declare_schema/model/table_options_definition'
5
+
6
+ RSpec.describe DeclareSchema::Model::TableOptionsDefinition do
7
+ before do
8
+ load File.expand_path('../prepare_testapp.rb', __dir__)
9
+
10
+ class TableOptionsDefinitionTestModel < ActiveRecord::Base
11
+ fields do
12
+ name :string, limit: 127, index: true
13
+ end
14
+ end
15
+ end
16
+
17
+ let(:model_class) { TableOptionsDefinitionTestModel }
18
+
19
+ context 'instance methods' do
20
+ let(:table_options) { { charset: "utf8", collation: "utf8_general"} }
21
+ let(:model) { described_class.new('table_options_definition_test_models', table_options) }
22
+
23
+ describe '#to_key' do
24
+ subject { model.to_key }
25
+ it { should eq(["table_options_definition_test_models", "{:charset=>\"utf8\", :collation=>\"utf8_general\"}"]) }
26
+ end
27
+
28
+ describe '#settings' do
29
+ subject { model.settings }
30
+ it { should eq("CHARACTER SET utf8 COLLATE utf8_general") }
31
+ end
32
+
33
+ describe '#hash' do
34
+ subject { model.hash }
35
+ it { should eq(["table_options_definition_test_models", "{:charset=>\"utf8\", :collation=>\"utf8_general\"}"].hash) }
36
+ end
37
+
38
+ describe '#to_s' do
39
+ subject { model.to_s }
40
+ it { should eq("CHARACTER SET utf8 COLLATE utf8_general") }
41
+ end
42
+
43
+ describe '#alter_table_statement' do
44
+ subject { model.alter_table_statement }
45
+ it { should eq('execute "ALTER TABLE \"table_options_definition_test_models\" CHARACTER SET utf8 COLLATE utf8_general;"') }
46
+ end
47
+ end
48
+
49
+
50
+ context 'class << self' do
51
+ describe '#for_model' do
52
+ context 'when using a SQLite connection' do
53
+ subject { described_class.for_model(model_class) }
54
+ it { should eq(described_class.new(model_class.table_name, {})) }
55
+ end
56
+ # TODO: Convert these tests to run against a MySQL database so that we can
57
+ # perform them without mocking out so much
58
+ context 'when using a MySQL connection' do
59
+ before do
60
+ double(ActiveRecord::ConnectionAdapters::Mysql2Adapter).tap do |stub_connection|
61
+ expect(stub_connection).to receive(:class).and_return(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
62
+ expect(stub_connection).to receive(:current_database).and_return('test_database')
63
+ expect(stub_connection).to receive(:quote_string).with('test_database').and_return('"test_database"')
64
+ expect(stub_connection).to receive(:quote_string).with(model_class.table_name).and_return("\"#{model_class.table_name}\"")
65
+ expect(stub_connection).to(
66
+ receive(:select_one).with(<<~EOS)
67
+ SELECT CCSA.character_set_name, CCSA.collation_name
68
+ FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
69
+ WHERE CCSA.collation_name = T.table_collation AND
70
+ T.table_schema = "test_database" AND
71
+ T.table_name = "#{model_class.table_name}";
72
+ EOS
73
+ .and_return({ "character_set_name" => "utf8", "collation_name" => "utf8_general" })
74
+ )
75
+ allow(model_class).to receive(:connection).and_return(stub_connection)
76
+ end
77
+ end
78
+
79
+ subject { described_class.for_model(model_class) }
80
+ it { should eq(described_class.new(model_class.table_name, { charset: "utf8", collation: "utf8_general" })) }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -43,6 +43,34 @@ module Generators
43
43
  end
44
44
  end
45
45
 
46
+ describe '#default_charset' do
47
+ subject { described_class.default_charset }
48
+
49
+ context 'when not explicitly set' do
50
+ it { should eq(:utf8mb4) }
51
+ end
52
+
53
+ context 'when explicitly set' do
54
+ before { described_class.default_charset = :utf8 }
55
+ after { described_class.default_charset = described_class::DEFAULT_CHARSET }
56
+ it { should eq(:utf8) }
57
+ end
58
+ end
59
+
60
+ describe '#default_collation' do
61
+ subject { described_class.default_collation }
62
+
63
+ context 'when not explicitly set' do
64
+ it { should eq(:utf8mb4_general) }
65
+ end
66
+
67
+ context 'when explicitly set' do
68
+ before { described_class.default_collation = :utf8mb4_general_ci }
69
+ after { described_class.default_collation = described_class::DEFAULT_COLLATION }
70
+ it { should eq(:utf8mb4_general_ci) }
71
+ end
72
+ end
73
+
46
74
  describe 'load_rails_models' do
47
75
  before do
48
76
  expect(Rails.application).to receive(:eager_load!)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: declare_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca Development adapted from hobo_fields by Tom Locke
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-05 00:00:00.000000000 Z
11
+ date: 2020-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -32,7 +32,7 @@ executables:
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
- - ".dependabot/config.yml"
35
+ - ".github/dependabot.yml"
36
36
  - ".github/workflows/gem_release.yml"
37
37
  - ".gitignore"
38
38
  - ".rspec"
@@ -61,6 +61,7 @@ files:
61
61
  - lib/declare_schema/model/field_spec.rb
62
62
  - lib/declare_schema/model/foreign_key_definition.rb
63
63
  - lib/declare_schema/model/index_definition.rb
64
+ - lib/declare_schema/model/table_options_definition.rb
64
65
  - lib/declare_schema/railtie.rb
65
66
  - lib/declare_schema/version.rb
66
67
  - lib/generators/declare_schema/migration/USAGE
@@ -78,6 +79,7 @@ files:
78
79
  - spec/lib/declare_schema/interactive_primary_key_spec.rb
79
80
  - spec/lib/declare_schema/migration_generator_spec.rb
80
81
  - spec/lib/declare_schema/model/index_definition_spec.rb
82
+ - spec/lib/declare_schema/model/table_options_definition_spec.rb
81
83
  - spec/lib/declare_schema/prepare_testapp.rb
82
84
  - spec/lib/generators/declare_schema/migration/migrator_spec.rb
83
85
  - spec/spec_helper.rb
@@ -87,7 +89,7 @@ homepage: https://github.com/Invoca/declare_schema
87
89
  licenses: []
88
90
  metadata:
89
91
  allowed_push_host: https://rubygems.org
90
- post_install_message:
92
+ post_install_message:
91
93
  rdoc_options: []
92
94
  require_paths:
93
95
  - lib
@@ -103,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
105
  version: 1.3.6
104
106
  requirements: []
105
107
  rubygems_version: 3.0.3
106
- signing_key:
108
+ signing_key:
107
109
  specification_version: 4
108
110
  summary: Database migration generator for Rails
109
111
  test_files: []
@@ -1,10 +0,0 @@
1
- ---
2
- version: 1
3
- update_configs:
4
- - package_manager: "ruby:bundler"
5
- directory: "/"
6
- update_schedule: "weekly"
7
- version_requirement_updates: "off"
8
- commit_message:
9
- prefix: "No-Jira"
10
- include_scope: true