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