strong_migrations 0.8.0 → 1.0.0

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