strong_migrations 0.8.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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