strong_migrations 0.7.8 → 1.0.0
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 +4 -4
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +1 -1
- data/README.md +139 -142
- data/lib/strong_migrations/adapters/abstract_adapter.rb +61 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +29 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +87 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +221 -0
- data/lib/strong_migrations/checker.rb +111 -516
- data/lib/strong_migrations/checks.rb +402 -0
- data/lib/strong_migrations/database_tasks.rb +2 -1
- data/lib/strong_migrations/error_messages.rb +222 -0
- data/lib/strong_migrations/migration.rb +7 -2
- data/lib/strong_migrations/migrator.rb +19 -0
- data/lib/strong_migrations/safe_methods.rb +5 -16
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +16 -218
- metadata +14 -77
@@ -0,0 +1,29 @@
|
|
1
|
+
module StrongMigrations
|
2
|
+
module Adapters
|
3
|
+
class MariaDBAdapter < MySQLAdapter
|
4
|
+
def name
|
5
|
+
"MariaDB"
|
6
|
+
end
|
7
|
+
|
8
|
+
def min_version
|
9
|
+
"10.2"
|
10
|
+
end
|
11
|
+
|
12
|
+
def server_version
|
13
|
+
@server_version ||= begin
|
14
|
+
target_version(StrongMigrations.target_mariadb_version) do
|
15
|
+
select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_statement_timeout(timeout)
|
21
|
+
select_all("SET max_statement_time = #{connection.quote(timeout)}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_column_default_safe?
|
25
|
+
server_version >= Gem::Version.new("10.3.2")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# note: MariaDB inherits from this adapter
|
2
|
+
# when making changes, be sure to see how it affects it
|
3
|
+
module StrongMigrations
|
4
|
+
module Adapters
|
5
|
+
class MySQLAdapter < AbstractAdapter
|
6
|
+
def name
|
7
|
+
"MySQL"
|
8
|
+
end
|
9
|
+
|
10
|
+
def min_version
|
11
|
+
"5.7"
|
12
|
+
end
|
13
|
+
|
14
|
+
def server_version
|
15
|
+
@server_version ||= begin
|
16
|
+
target_version(StrongMigrations.target_mysql_version) do
|
17
|
+
select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_statement_timeout(timeout)
|
23
|
+
# use ceil to prevent no timeout for values under 1 ms
|
24
|
+
select_all("SET max_execution_time = #{connection.quote((timeout.to_f * 1000).ceil)}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_lock_timeout(timeout)
|
28
|
+
select_all("SET lock_wait_timeout = #{connection.quote(timeout)}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_lock_timeout(limit)
|
32
|
+
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
33
|
+
# lock timeout is an integer
|
34
|
+
if lock_timeout.to_i > limit
|
35
|
+
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def analyze_table(table)
|
40
|
+
connection.execute "ANALYZE TABLE #{connection.quote_table_name(table.to_s)}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_column_default_safe?
|
44
|
+
server_version >= Gem::Version.new("8.0.12")
|
45
|
+
end
|
46
|
+
|
47
|
+
def change_type_safe?(table, column, type, options, existing_column, existing_type)
|
48
|
+
safe = false
|
49
|
+
|
50
|
+
case type.to_s
|
51
|
+
when "string"
|
52
|
+
# https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
|
53
|
+
# https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
|
54
|
+
# increased limit, but doesn't change number of length bytes
|
55
|
+
# 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
|
56
|
+
limit = options[:limit] || 255
|
57
|
+
safe = ["varchar"].include?(existing_type) &&
|
58
|
+
limit >= existing_column.limit &&
|
59
|
+
(limit <= 255 || existing_column.limit > 255)
|
60
|
+
end
|
61
|
+
|
62
|
+
safe
|
63
|
+
end
|
64
|
+
|
65
|
+
def strict_mode?
|
66
|
+
sql_modes = sql_modes()
|
67
|
+
sql_modes.include?("STRICT_ALL_TABLES") || sql_modes.include?("STRICT_TRANS_TABLES")
|
68
|
+
end
|
69
|
+
|
70
|
+
def rewrite_blocks
|
71
|
+
"writes"
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# do not memoize
|
77
|
+
# want latest value
|
78
|
+
def sql_modes
|
79
|
+
if StrongMigrations.target_sql_mode && StrongMigrations.developer_env?
|
80
|
+
StrongMigrations.target_sql_mode.split(",")
|
81
|
+
else
|
82
|
+
select_all("SELECT @@SESSION.sql_mode").first["@@SESSION.sql_mode"].split(",")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module StrongMigrations
|
2
|
+
module Adapters
|
3
|
+
class PostgreSQLAdapter < AbstractAdapter
|
4
|
+
def name
|
5
|
+
"PostgreSQL"
|
6
|
+
end
|
7
|
+
|
8
|
+
def min_version
|
9
|
+
"10"
|
10
|
+
end
|
11
|
+
|
12
|
+
def server_version
|
13
|
+
@version ||= begin
|
14
|
+
target_version(StrongMigrations.target_postgresql_version) do
|
15
|
+
version = select_all("SHOW server_version_num").first["server_version_num"].to_i
|
16
|
+
if version >= 100000
|
17
|
+
# major version for 10+
|
18
|
+
"#{version / 10000}"
|
19
|
+
else
|
20
|
+
# major and minor version for < 10
|
21
|
+
"#{version / 10000}.#{(version % 10000) / 100}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_statement_timeout(timeout)
|
28
|
+
set_timeout("statement_timeout", timeout)
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_lock_timeout(timeout)
|
32
|
+
set_timeout("lock_timeout", timeout)
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_lock_timeout(limit)
|
36
|
+
lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
|
37
|
+
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
38
|
+
if lock_timeout_sec == 0
|
39
|
+
warn "[strong_migrations] DANGER: No lock timeout set"
|
40
|
+
elsif lock_timeout_sec > limit
|
41
|
+
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def analyze_table(table)
|
46
|
+
connection.execute "ANALYZE #{connection.quote_table_name(table.to_s)}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_column_default_safe?
|
50
|
+
server_version >= Gem::Version.new("11")
|
51
|
+
end
|
52
|
+
|
53
|
+
def change_type_safe?(table, column, type, options, existing_column, existing_type)
|
54
|
+
safe = false
|
55
|
+
|
56
|
+
case type.to_s
|
57
|
+
when "string"
|
58
|
+
# safe to increase limit or remove it
|
59
|
+
# not safe to decrease limit or add a limit
|
60
|
+
case existing_type
|
61
|
+
when "character varying"
|
62
|
+
safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
|
63
|
+
when "text"
|
64
|
+
safe = !options[:limit]
|
65
|
+
when "citext"
|
66
|
+
safe = !options[:limit] && !indexed?(table, column)
|
67
|
+
end
|
68
|
+
when "text"
|
69
|
+
# safe to change varchar to text (and text to text)
|
70
|
+
safe =
|
71
|
+
["character varying", "text"].include?(existing_type) ||
|
72
|
+
(existing_type == "citext" && !indexed?(table, column))
|
73
|
+
when "citext"
|
74
|
+
safe = ["character varying", "text"].include?(existing_type) && !indexed?(table, column)
|
75
|
+
when "varbit"
|
76
|
+
# increasing length limit or removing the limit is safe
|
77
|
+
# but there doesn't seem to be a way to set/modify it
|
78
|
+
# https://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.2#Reduce_ALTER_TABLE_rewrites
|
79
|
+
when "numeric", "decimal"
|
80
|
+
# numeric and decimal are equivalent and can be used interchangably
|
81
|
+
safe = ["numeric", "decimal"].include?(existing_type) &&
|
82
|
+
(
|
83
|
+
(
|
84
|
+
# unconstrained
|
85
|
+
!options[:precision] && !options[:scale]
|
86
|
+
) || (
|
87
|
+
# increased precision, same scale
|
88
|
+
options[:precision] && existing_column.precision &&
|
89
|
+
options[:precision] >= existing_column.precision &&
|
90
|
+
options[:scale] == existing_column.scale
|
91
|
+
)
|
92
|
+
)
|
93
|
+
when "datetime", "timestamp", "timestamptz"
|
94
|
+
# precision for datetime
|
95
|
+
# limit for timestamp, timestamptz
|
96
|
+
precision = (type.to_s == "datetime" ? options[:precision] : options[:limit]) || 6
|
97
|
+
existing_precision = existing_column.limit || existing_column.precision || 6
|
98
|
+
|
99
|
+
type_map = {
|
100
|
+
"timestamp" => "timestamp without time zone",
|
101
|
+
"timestamptz" => "timestamp with time zone"
|
102
|
+
}
|
103
|
+
maybe_safe = type_map.values.include?(existing_type) && precision >= existing_precision
|
104
|
+
|
105
|
+
if maybe_safe
|
106
|
+
new_type = type.to_s == "datetime" ? datetime_type : type.to_s
|
107
|
+
|
108
|
+
# resolve with fallback
|
109
|
+
new_type = type_map[new_type] || new_type
|
110
|
+
|
111
|
+
safe = new_type == existing_type || (server_version >= Gem::Version.new("12") && time_zone == "UTC")
|
112
|
+
end
|
113
|
+
when "time"
|
114
|
+
precision = options[:precision] || options[:limit] || 6
|
115
|
+
existing_precision = existing_column.precision || existing_column.limit || 6
|
116
|
+
|
117
|
+
safe = existing_type == "time without time zone" && precision >= existing_precision
|
118
|
+
when "timetz"
|
119
|
+
# increasing precision is safe
|
120
|
+
# but there doesn't seem to be a way to set/modify it
|
121
|
+
when "interval"
|
122
|
+
# https://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.2#Reduce_ALTER_TABLE_rewrites
|
123
|
+
# Active Record uses precision before limit
|
124
|
+
precision = options[:precision] || options[:limit] || 6
|
125
|
+
existing_precision = existing_column.precision || existing_column.limit || 6
|
126
|
+
|
127
|
+
safe = existing_type == "interval" && precision >= existing_precision
|
128
|
+
when "inet"
|
129
|
+
safe = existing_type == "cidr"
|
130
|
+
end
|
131
|
+
|
132
|
+
safe
|
133
|
+
end
|
134
|
+
|
135
|
+
def constraints(table_name)
|
136
|
+
query = <<~SQL
|
137
|
+
SELECT
|
138
|
+
conname AS name,
|
139
|
+
pg_get_constraintdef(oid) AS def
|
140
|
+
FROM
|
141
|
+
pg_constraint
|
142
|
+
WHERE
|
143
|
+
contype = 'c' AND
|
144
|
+
convalidated AND
|
145
|
+
conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
|
146
|
+
SQL
|
147
|
+
select_all(query.squish).to_a
|
148
|
+
end
|
149
|
+
|
150
|
+
def writes_blocked?
|
151
|
+
query = <<~SQL
|
152
|
+
SELECT
|
153
|
+
relation::regclass::text
|
154
|
+
FROM
|
155
|
+
pg_locks
|
156
|
+
WHERE
|
157
|
+
mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
|
158
|
+
pid = pg_backend_pid()
|
159
|
+
SQL
|
160
|
+
select_all(query.squish).any?
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def set_timeout(setting, timeout)
|
166
|
+
# use ceil to prevent no timeout for values under 1 ms
|
167
|
+
timeout = (timeout.to_f * 1000).ceil unless timeout.is_a?(String)
|
168
|
+
|
169
|
+
select_all("SET #{setting} TO #{connection.quote(timeout)}")
|
170
|
+
end
|
171
|
+
|
172
|
+
# units: https://www.postgresql.org/docs/current/config-setting.html
|
173
|
+
def timeout_to_sec(timeout)
|
174
|
+
units = {
|
175
|
+
"us" => 0.001,
|
176
|
+
"ms" => 1,
|
177
|
+
"s" => 1000,
|
178
|
+
"min" => 1000 * 60,
|
179
|
+
"h" => 1000 * 60 * 60,
|
180
|
+
"d" => 1000 * 60 * 60 * 24
|
181
|
+
}
|
182
|
+
timeout_ms = timeout.to_i
|
183
|
+
units.each do |k, v|
|
184
|
+
if timeout.end_with?(k)
|
185
|
+
timeout_ms *= v
|
186
|
+
break
|
187
|
+
end
|
188
|
+
end
|
189
|
+
timeout_ms / 1000.0
|
190
|
+
end
|
191
|
+
|
192
|
+
# columns is array for column index and string for expression index
|
193
|
+
# the current approach can yield false positives for expression indexes
|
194
|
+
# but prefer to keep it simple for now
|
195
|
+
def indexed?(table, column)
|
196
|
+
connection.indexes(table).any? { |i| i.columns.include?(column.to_s) }
|
197
|
+
end
|
198
|
+
|
199
|
+
def datetime_type
|
200
|
+
key =
|
201
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
202
|
+
# https://github.com/rails/rails/pull/41084
|
203
|
+
# no need to support custom datetime_types
|
204
|
+
connection.class.datetime_type
|
205
|
+
else
|
206
|
+
# https://github.com/rails/rails/issues/21126#issuecomment-327895275
|
207
|
+
:datetime
|
208
|
+
end
|
209
|
+
|
210
|
+
# could be timestamp, timestamp without time zone, timestamp with time zone, etc
|
211
|
+
connection.class.const_get(:NATIVE_DATABASE_TYPES).fetch(key).fetch(:name)
|
212
|
+
end
|
213
|
+
|
214
|
+
# do not memoize
|
215
|
+
# want latest value
|
216
|
+
def time_zone
|
217
|
+
select_all("SHOW timezone").first["TimeZone"]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|