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.
@@ -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