declare_schema 0.4.1 → 0.5.0

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: 2e1d3c68c4e9b2f6ce739eba37ed19c5f80204495a50a9240e9f1f68a09777e3
4
- data.tar.gz: 63e0a3205c82c2b5e6dacbfbfd10e9aa2e63eee5898d11fd5c102618e73755aa
3
+ metadata.gz: 59ff5d6d1a71b80a0541e2430f77665914d0c354ec92f31db8bbf22630fa5bef
4
+ data.tar.gz: dcaedebe325290c8c625c099b4d3cd6e2a87694f1d3cfec68811618f635715c1
5
5
  SHA512:
6
- metadata.gz: 34ab74d97dd53c426d3a289b622670a42846d84279c2e3967ad0663c8f4871b1470a90b309237b09b09724a7e7c2405fb0f614a00d33902f8000bb1df23459ef
7
- data.tar.gz: 2bf6ca852f71807e362ef26b4ba14d75b8a452a6245094eb8023fa1889b94d6584cf15856962f9fb85168208e0de4dd23a7942fb18cdcc5335f7c744382ba826
6
+ metadata.gz: 0a62a4236edd68e1429d4949b6745f34ed186acc24935bb027d1108b59372c0b681abc514963730c8e9e092769aa4a614a286c8815aa704a8dc24378e6cbe4ee
7
+ data.tar.gz: f8a9b7e40ffe14aa35e0a33d66c4e7dd820535ceacbc488bf159b386e28f5edf0d69896ff2e96a2c521e41e59c36fb9d7f47a6d1b6089a3625d8664a41be5b95
@@ -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,15 @@ 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] - 2020-12-21
8
+ ### Added
9
+ - Added support for configuring the character set and collation for MySQL databases
10
+ at the global, table, and field level
11
+
12
+ ## [0.4.2] - 2020-12-05
13
+ ### Fixed
14
+ - Generalize the fix below to sqlite || Rails 4.
15
+
7
16
  ## [0.4.1] - 2020-12-04
8
17
  ### Fixed
9
18
  - Fixed a bug detecting compound primary keys in Rails 4.
@@ -11,7 +20,7 @@ Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0
11
20
  ## [0.4.0] - 2020-11-20
12
21
  ### Added
13
22
  - Fields may be declared with `serialize: true` (any value with a valid `.to_yaml` stored as YAML),
14
- or `serialize: <serializeable-class>`, where `<serializeable-class>`
23
+ or `serialize: <serializeable-class>`, where `<serializeable-class>`
15
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)
16
25
  or any custom serializable class.
17
26
  This invokes `ActiveSupport`'s `serialize` macro for that field, passing the serializable class, if given.
@@ -64,6 +73,8 @@ using the appropriate Rails configuration attributes.
64
73
  ### Added
65
74
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
66
75
 
76
+ [0.5.0]: https://github.com/Invoca/declare_schema/compare/v0.4.2...v0.5.0
77
+ [0.4.2]: https://github.com/Invoca/declare_schema/compare/v0.4.1...v0.4.2
67
78
  [0.4.1]: https://github.com/Invoca/declare_schema/compare/v0.4.0...v0.4.1
68
79
  [0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
69
80
  [0.3.1]: https://github.com/Invoca/declare_schema/compare/v0.3.0...v0.3.1
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.1)
4
+ declare_schema (0.5.0)
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::Migration::Migrator.default_charset = "utf8mb4"
97
+ Generators::DeclareSchema::Migration::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)
@@ -62,9 +62,9 @@ module DeclareSchema
62
62
 
63
63
  private
64
64
 
65
- # This is the old approach which is still needed for SQLite
65
+ # This is the old approach which is still needed for MySQL in Rails 4 and SQLite
66
66
  def sqlite_compound_primary_key(model, table)
67
- ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) or return nil
67
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) || Rails::VERSION::MAJOR < 5 or return nil
68
68
 
69
69
  connection = model.connection.dup
70
70
 
@@ -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.1"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -23,6 +23,10 @@ module Generators
23
23
  end
24
24
  end
25
25
 
26
+ def table_options
27
+ {}
28
+ end
29
+
26
30
  def table_name
27
31
  join_table
28
32
  end
@@ -69,13 +73,19 @@ module Generators
69
73
  class Migrator
70
74
  class Error < RuntimeError; end
71
75
 
72
- @ignore_models = []
73
- @ignore_tables = []
76
+ DEFAULT_CHARSET = :utf8mb4
77
+ DEFAULT_COLLATION = :utf8mb4_general
78
+
79
+ @ignore_models = []
80
+ @ignore_tables = []
74
81
  @before_generating_migration_callback = nil
75
- @active_record_class = ActiveRecord::Base
82
+ @active_record_class = ActiveRecord::Base
83
+ @default_charset = DEFAULT_CHARSET
84
+ @default_collation = DEFAULT_COLLATION
76
85
 
77
86
  class << self
78
- attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints, :active_record_class
87
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints,
88
+ :active_record_class, :default_charset, :default_collation
79
89
  attr_reader :before_generating_migration_callback
80
90
 
81
91
  def active_record_class
@@ -292,52 +302,76 @@ module Generators
292
302
  "drop_table :#{t}"
293
303
  end * "\n"
294
304
 
295
- changes = []
296
- undo_changes = []
297
- index_changes = []
298
- undo_index_changes = []
299
- fk_changes = []
300
- undo_fk_changes = []
305
+ changes = []
306
+ undo_changes = []
307
+ index_changes = []
308
+ undo_index_changes = []
309
+ fk_changes = []
310
+ undo_fk_changes = []
311
+ table_options_changes = []
312
+ undo_table_options_changes = []
313
+
301
314
  to_change.each do |t|
302
315
  model = models_by_table_name[t]
303
316
  table = to_rename.key(t) || model.table_name
304
317
  if table.in?(db_tables)
305
- change, undo, index_change, undo_index, fk_change, undo_fk = change_table(model, table)
318
+ change, undo, index_change, undo_index, fk_change, undo_fk, table_options_change, undo_table_options_change = change_table(model, table)
306
319
  changes << change
307
320
  undo_changes << undo
308
321
  index_changes << index_change
309
322
  undo_index_changes << undo_index
310
323
  fk_changes << fk_change
311
324
  undo_fk_changes << undo_fk
325
+ table_options_changes << table_options_change
326
+ undo_table_options_changes << undo_table_options_change
312
327
  end
313
328
  end
314
329
 
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"
330
+ up = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten.reject(&:blank?) * "\n\n"
331
+ 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
332
 
318
333
  [up, down]
319
334
  end
320
335
 
321
336
  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}"
337
+ longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
338
+ disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
339
+ table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
340
+ field_definitions = [
341
+ disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
342
+ *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
343
+ ].compact
344
+
345
+ <<~EOS.strip
346
+ create_table :#{model.table_name}, #{create_table_options(model, disable_auto_increment)} do |t|
347
+ #{field_definitions.join("\n")}
331
348
  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"
349
+
350
+ #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
351
+ #{create_indexes(model).join("\n") unless Migrator.disable_indexing}
352
+ #{create_constraints(model).join("\n") unless Migrator.disable_indexing}
353
+ EOS
354
+ end
355
+
356
+ def create_table_options(model, disable_auto_increment)
357
+ if model.primary_key.blank? || disable_auto_increment
358
+ "id: false"
359
+ elsif model.primary_key == "id"
360
+ "id: :bigint"
361
+ else
362
+ "primary_key: :#{model.primary_key}"
363
+ end
364
+ end
365
+
366
+ def table_options_for_model(model)
367
+ if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
368
+ {}
369
+ else
370
+ {
371
+ charset: model.table_options[:charset] || Migrator.default_charset,
372
+ collation: model.table_options[:collation] || Migrator.default_collation
373
+ }
374
+ end
341
375
  end
342
376
 
343
377
  def create_indexes(model)
@@ -351,7 +385,7 @@ module Generators
351
385
  def create_field(field_spec, field_name_width)
352
386
  options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
353
387
  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(', '))
388
+ format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
355
389
  end
356
390
 
357
391
  def change_table(model, current_table_name)
@@ -413,15 +447,17 @@ module Generators
413
447
  col_name = old_names[c] || c
414
448
  col = db_columns[col_name]
415
449
  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
450
+ 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
451
  change_spec = fk_field_options(model, c)
418
452
  change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
419
453
  ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
420
454
  (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?
455
+ change_spec[:precision] = spec.precision unless spec.precision.nil?
456
+ change_spec[:scale] = spec.scale unless spec.scale.nil?
457
+ change_spec[:null] = spec.null unless spec.null && col.null
458
+ change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
459
+ change_spec[:collation] = spec.collation unless spec.collation.nil?
460
+ change_spec[:charset] = spec.charset unless spec.charset.nil?
425
461
 
426
462
  changes << "change_column :#{new_table_name}, :#{c}, " +
427
463
  ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
@@ -436,13 +472,20 @@ module Generators
436
472
  else
437
473
  change_foreign_key_constraints(model, current_table_name)
438
474
  end
475
+ table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
476
+ change_table_options(model, current_table_name)
477
+ else
478
+ [[], []]
479
+ end
439
480
 
440
481
  [(renames + adds + removes + changes) * "\n",
441
482
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
442
483
  index_changes * "\n",
443
484
  undo_index_changes * "\n",
444
485
  fk_changes * "\n",
445
- undo_fk_changes * "\n"]
486
+ undo_fk_changes * "\n",
487
+ table_options_changes * "\n",
488
+ undo_table_options_changes * "\n"]
446
489
  end
447
490
 
448
491
  def change_indexes(model, old_table_name)
@@ -552,6 +595,20 @@ module Generators
552
595
  end
553
596
  end
554
597
 
598
+ def change_table_options(model, current_table_name)
599
+ old_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.for_model(model, current_table_name)
600
+ new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
601
+
602
+ if old_options_definition.equivalent?(new_options_definition)
603
+ [[], []]
604
+ else
605
+ [
606
+ [new_options_definition.alter_table_statement],
607
+ [old_options_definition.alter_table_statement]
608
+ ]
609
+ end
610
+ end
611
+
555
612
  def revert_table(table)
556
613
  res = StringIO.new
557
614
  schema_dumper_klass = case Rails::VERSION::MAJOR
@@ -27,15 +27,28 @@ RSpec.describe 'DeclareSchema Migration Generator' do
27
27
  end
28
28
  EOS
29
29
 
30
- expect_test_definition_to_eq('alpha/beta', <<~EOS)
31
- require 'test_helper'
32
-
33
- class Alpha::BetaTest < ActiveSupport::TestCase
34
- # test "the truth" do
35
- # assert true
36
- # end
37
- end
38
- EOS
30
+ case Rails::VERSION::MAJOR
31
+ when 4, 5
32
+ expect_test_definition_to_eq('alpha/beta', <<~EOS)
33
+ require 'test_helper'
34
+
35
+ class Alpha::BetaTest < ActiveSupport::TestCase
36
+ # test "the truth" do
37
+ # assert true
38
+ # end
39
+ end
40
+ EOS
41
+ else
42
+ expect_test_definition_to_eq('alpha/beta', <<~EOS)
43
+ require "test_helper"
44
+
45
+ class Alpha::BetaTest < ActiveSupport::TestCase
46
+ # test "the truth" do
47
+ # assert true
48
+ # end
49
+ end
50
+ EOS
51
+ end
39
52
 
40
53
  case Rails::VERSION::MAJOR
41
54
  when 4
@@ -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.1
4
+ version: 0.5.0
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-21 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