strong_migrations 0.7.8 → 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,29 @@
1
+ module StrongMigrations
2
+ module Adapters
3
+ class MariaDBAdapter < MySQLAdapter
4
+ def name
5
+ "MariaDB"
6
+ end
7
+
8
+ def min_version
9
+ "10.2"
10
+ end
11
+
12
+ def server_version
13
+ @server_version ||= begin
14
+ target_version(StrongMigrations.target_mariadb_version) do
15
+ select_all("SELECT VERSION()").first["VERSION()"].split("-").first
16
+ end
17
+ end
18
+ end
19
+
20
+ def set_statement_timeout(timeout)
21
+ select_all("SET max_statement_time = #{connection.quote(timeout)}")
22
+ end
23
+
24
+ def add_column_default_safe?
25
+ server_version >= Gem::Version.new("10.3.2")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,87 @@
1
+ # note: MariaDB inherits from this adapter
2
+ # when making changes, be sure to see how it affects it
3
+ module StrongMigrations
4
+ module Adapters
5
+ class MySQLAdapter < AbstractAdapter
6
+ def name
7
+ "MySQL"
8
+ end
9
+
10
+ def min_version
11
+ "5.7"
12
+ end
13
+
14
+ def server_version
15
+ @server_version ||= begin
16
+ target_version(StrongMigrations.target_mysql_version) do
17
+ select_all("SELECT VERSION()").first["VERSION()"].split("-").first
18
+ end
19
+ end
20
+ end
21
+
22
+ def set_statement_timeout(timeout)
23
+ # use ceil to prevent no timeout for values under 1 ms
24
+ select_all("SET max_execution_time = #{connection.quote((timeout.to_f * 1000).ceil)}")
25
+ end
26
+
27
+ def set_lock_timeout(timeout)
28
+ select_all("SET lock_wait_timeout = #{connection.quote(timeout)}")
29
+ end
30
+
31
+ def check_lock_timeout(limit)
32
+ lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
33
+ # lock timeout is an integer
34
+ if lock_timeout.to_i > limit
35
+ warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
36
+ end
37
+ end
38
+
39
+ def analyze_table(table)
40
+ connection.execute "ANALYZE TABLE #{connection.quote_table_name(table.to_s)}"
41
+ end
42
+
43
+ def add_column_default_safe?
44
+ server_version >= Gem::Version.new("8.0.12")
45
+ end
46
+
47
+ def change_type_safe?(table, column, type, options, existing_column, existing_type)
48
+ safe = false
49
+
50
+ case type.to_s
51
+ when "string"
52
+ # https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
53
+ # https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
54
+ # increased limit, but doesn't change number of length bytes
55
+ # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
56
+ limit = options[:limit] || 255
57
+ safe = ["varchar"].include?(existing_type) &&
58
+ limit >= existing_column.limit &&
59
+ (limit <= 255 || existing_column.limit > 255)
60
+ end
61
+
62
+ safe
63
+ end
64
+
65
+ def strict_mode?
66
+ sql_modes = sql_modes()
67
+ sql_modes.include?("STRICT_ALL_TABLES") || sql_modes.include?("STRICT_TRANS_TABLES")
68
+ end
69
+
70
+ def rewrite_blocks
71
+ "writes"
72
+ end
73
+
74
+ private
75
+
76
+ # do not memoize
77
+ # want latest value
78
+ def sql_modes
79
+ if StrongMigrations.target_sql_mode && StrongMigrations.developer_env?
80
+ StrongMigrations.target_sql_mode.split(",")
81
+ else
82
+ select_all("SELECT @@SESSION.sql_mode").first["@@SESSION.sql_mode"].split(",")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -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