declare_schema 0.4.2 → 0.5.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: 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