strong_migrations 0.8.0 → 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.
@@ -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