Skip to content

Commit 40c68d7

Browse files
authored
Execute NOT NULL with ADD COLUMN in PG11+ (#39)
* Execute NOT NULL with ADD COLUMN in PG11+ One of the performance improvements in Postgres 11 was executing an `ADD COLUMN ... NOT NULL` without needing a table rewrite. This is now a safe operation in PG11+, and we can avoid adding backwards-compatible safe version with a statement timeout that can fail in some cases. https://www.postgresql.org/docs/release/11.0/ * Fix variable naming * Ignore Metrics/PerceivedComplexity for add_column * Remove unnecessary check * Add more tests around add_column for older PG * Refactor add_column to early out for PG11+ This simplifies the if checks enough that we no longer have to ignore the Rubocop complaints about complexity.
1 parent 22926c1 commit 40c68d7

File tree

2 files changed

+101
-14
lines changed

2 files changed

+101
-14
lines changed

lib/safe-pg-migrations/plugins/statement_insurer.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ module StatementInsurer
1010
end
1111
end
1212

13-
def add_column(table_name, column_name, type, **options) # rubocop:disable Metrics/CyclomaticComplexity
14-
need_default_value_backfill = SafePgMigrations.pg_version_num < PG_11_VERSION_NUM
13+
def add_column(table_name, column_name, type, **options)
14+
return super if SafePgMigrations.pg_version_num >= PG_11_VERSION_NUM
1515

16-
default = options.delete(:default) if need_default_value_backfill
16+
default = options.delete(:default)
1717
null = options.delete(:null)
1818

1919
if !default.nil? || null == false
@@ -22,7 +22,7 @@ def add_column(table_name, column_name, type, **options) # rubocop:disable Metri
2222

2323
super
2424

25-
if need_default_value_backfill && !default.nil?
25+
unless default.nil?
2626
SafePgMigrations.say_method_call(:change_column_default, table_name, column_name, default)
2727
change_column_default(table_name, column_name, default)
2828

test/safe_pg_migrations_test.rb

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,101 @@ def up
120120
], calls[1..4].map(&:first)
121121
end
122122

123-
def test_add_column_before_pg_11
123+
def test_add_column_before_pg_11_without_null_or_default_value
124+
@connection.create_table(:users)
125+
@migration =
126+
Class.new(ActiveRecord::Migration::Current) do
127+
def up
128+
add_column(:users, :admin, :boolean)
129+
end
130+
end.new
131+
132+
SafePgMigrations.stub(:get_pg_version_num, 96_000) do
133+
execute_calls = nil
134+
write_calls =
135+
record_calls(@migration, :write) do
136+
execute_calls = record_calls(@connection, :execute) { run_migration }
137+
end
138+
assert_calls [
139+
# The column is added without any default.
140+
'ALTER TABLE "users" ADD "admin" boolean',
141+
], execute_calls
142+
143+
assert_equal [
144+
'== 8128 : migrating ===========================================================',
145+
'-- add_column(:users, :admin, :boolean)',
146+
], write_calls.map(&:first)[0...-3]
147+
end
148+
end
149+
150+
def test_add_column_before_pg_11_with_null_constraint
151+
@connection.create_table(:users)
152+
@migration =
153+
Class.new(ActiveRecord::Migration::Current) do
154+
def up
155+
add_column(:users, :admin, :boolean, null: false)
156+
end
157+
end.new
158+
159+
SafePgMigrations.stub(:get_pg_version_num, 96_000) do
160+
execute_calls = nil
161+
write_calls =
162+
record_calls(@migration, :write) do
163+
execute_calls = record_calls(@connection, :execute) { run_migration }
164+
end
165+
assert_calls [
166+
# The column is added without any default.
167+
'ALTER TABLE "users" ADD "admin" boolean',
168+
169+
# The not-null constraint is added.
170+
"SET statement_timeout TO '5s'",
171+
'ALTER TABLE "users" ALTER COLUMN "admin" SET NOT NULL',
172+
"SET statement_timeout TO '70s'",
173+
], execute_calls
174+
175+
assert_equal [
176+
'== 8128 : migrating ===========================================================',
177+
'-- add_column(:users, :admin, :boolean, {:null=>false})',
178+
' -> add_column("users", :admin, :boolean, {})',
179+
' -> change_column_null("users", :admin, false)',
180+
], write_calls.map(&:first)[0...-3]
181+
end
182+
end
183+
184+
def test_add_column_before_pg_11_with_default_value
185+
@connection.create_table(:users)
186+
@migration =
187+
Class.new(ActiveRecord::Migration::Current) do
188+
def up
189+
add_column(:users, :admin, :boolean, default: false)
190+
end
191+
end.new
192+
193+
SafePgMigrations.stub(:get_pg_version_num, 96_000) do
194+
execute_calls = nil
195+
write_calls =
196+
record_calls(@migration, :write) do
197+
execute_calls = record_calls(@connection, :execute) { run_migration }
198+
end
199+
assert_calls [
200+
# The column is added without any default.
201+
'ALTER TABLE "users" ADD "admin" boolean',
202+
203+
# The default is added.
204+
'ALTER TABLE "users" ALTER COLUMN "admin" SET DEFAULT FALSE',
205+
], execute_calls
206+
207+
assert_equal [
208+
'== 8128 : migrating ===========================================================',
209+
'-- add_column(:users, :admin, :boolean, {:default=>false})',
210+
' -> add_column("users", :admin, :boolean, {})',
211+
' -> change_column_default("users", :admin, false)',
212+
' -> backfill_column_default("users", :admin)',
213+
], write_calls.map(&:first)[0...-3]
214+
end
215+
end
216+
217+
def test_add_column_before_pg_11_with_null_and_default_value
124218
@connection.create_table(:users)
125219
@migration =
126220
Class.new(ActiveRecord::Migration::Current) do
@@ -175,20 +269,13 @@ def up
175269
execute_calls = record_calls(@connection, :execute) { run_migration }
176270
end
177271
assert_calls [
178-
# The column is added with the default without any trick
179-
'ALTER TABLE "users" ADD "admin" boolean DEFAULT FALSE',
180-
181-
# The not-null constraint is added.
182-
"SET statement_timeout TO '5s'",
183-
'ALTER TABLE "users" ALTER COLUMN "admin" SET NOT NULL',
184-
"SET statement_timeout TO '70s'",
272+
# The column is added with the default and not null constraint without any tricks
273+
'ALTER TABLE "users" ADD "admin" boolean DEFAULT FALSE NOT NULL',
185274
], execute_calls
186275

187276
assert_equal [
188277
'== 8128 : migrating ===========================================================',
189278
'-- add_column(:users, :admin, :boolean, {:default=>false, :null=>false})',
190-
' -> add_column("users", :admin, :boolean, {:default=>false})',
191-
' -> change_column_null("users", :admin, false)',
192279
], write_calls.map(&:first)[0...-3]
193280
end
194281
end

0 commit comments

Comments
 (0)