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 +4 -4
- data/.github/dependabot.yml +14 -0
- data/CHANGELOG.md +12 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +66 -0
- data/gemfiles/rails_4.gemfile +1 -0
- data/gemfiles/rails_5.gemfile +1 -0
- data/gemfiles/rails_6.gemfile +1 -0
- data/lib/declare_schema.rb +1 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +2 -1
- data/lib/declare_schema/model.rb +4 -0
- data/lib/declare_schema/model/field_spec.rb +82 -26
- data/lib/declare_schema/model/index_definition.rb +2 -2
- data/lib/declare_schema/model/table_options_definition.rb +83 -0
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +95 -38
- data/spec/lib/declare_schema/generator_spec.rb +22 -9
- data/spec/lib/declare_schema/model/table_options_definition_spec.rb +84 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +28 -0
- metadata +8 -6
- data/.dependabot/config.yml +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59ff5d6d1a71b80a0541e2430f77665914d0c354ec92f31db8bbf22630fa5bef
|
4
|
+
data.tar.gz: dcaedebe325290c8c625c099b4d3cd6e2a87694f1d3cfec68811618f635715c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/CHANGELOG.md
CHANGED
@@ -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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
declare_schema (0.
|
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:
|
data/gemfiles/rails_4.gemfile
CHANGED
data/gemfiles/rails_5.gemfile
CHANGED
data/gemfiles/rails_6.gemfile
CHANGED
data/lib/declare_schema.rb
CHANGED
@@ -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)
|
data/lib/declare_schema/model.rb
CHANGED
@@ -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
|
-
!
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
73
|
-
|
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
|
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,
|
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
|
323
|
-
disable_auto_increment
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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("
|
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]
|
422
|
-
change_spec[:scale]
|
423
|
-
change_spec[:null]
|
424
|
-
change_spec[:default]
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
+
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-
|
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
|
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: []
|