Skip to content

Commit 0ed9639

Browse files
author
Thomas Hareau
authored
Make create_table idempotent for indexes as well (#28)
* Make create_table idempotent for indexes as well * Add test were one index was already existing * Fix test
1 parent 2c35308 commit 0ed9639

File tree

4 files changed

+116
-41
lines changed

4 files changed

+116
-41
lines changed

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,19 @@ def add_foreign_key(from_table, to_table, **options)
4343
end
4444

4545
def create_table(table_name, comment: nil, **options)
46-
return super unless table_exists?(table_name)
46+
return super if options[:force] || !table_exists?(table_name)
4747

48-
SafePgMigrations.say(
49-
"/!\\ Table '#{table_name}' already exists. Skipping statement.",
50-
true
51-
)
48+
SafePgMigrations.say "/!\\ Table '#{table_name}' already exists.", true
49+
50+
td = create_table_definition(table_name, **options)
51+
52+
yield td if block_given?
53+
54+
SafePgMigrations.say(td.indexes.empty? ? '-- Skipping statement' : '-- Creating indexes', true)
55+
56+
td.indexes.each do |column_name, index_options|
57+
add_index(table_name, column_name, index_options)
58+
end
5259
end
5360

5461
private

test/create_table_test.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
5+
class SafePgMigrationsTest < Minitest::Test
6+
def setup
7+
SafePgMigrations.instance_variable_set(:@config, nil)
8+
@connection = ActiveRecord::Base.connection
9+
@verbose_was = ActiveRecord::Migration.verbose
10+
@connection.create_table(:schema_migrations) { |t| t.string :version }
11+
ActiveRecord::SchemaMigration.create_table
12+
ActiveRecord::Migration.verbose = false
13+
@connection.execute("SET statement_timeout TO '70s'")
14+
@connection.execute("SET lock_timeout TO '70s'")
15+
end
16+
17+
def teardown
18+
ActiveRecord::SchemaMigration.drop_table
19+
@connection.execute('SET statement_timeout TO 0')
20+
@connection.execute("SET lock_timeout TO '30s'")
21+
@connection.drop_table(:users, if_exists: true)
22+
ActiveRecord::Migration.verbose = @verbose_was
23+
end
24+
25+
def test_create_table
26+
@migration =
27+
Class.new(ActiveRecord::Migration::Current) do
28+
def change
29+
create_table(:users) do |t|
30+
t.string :email
31+
t.references :user, foreign_key: true
32+
end
33+
end
34+
end.new
35+
36+
calls = record_calls(@connection, :execute) { run_migration }
37+
assert_calls [
38+
"SET statement_timeout TO '5s'",
39+
40+
# Create the table with constraints.
41+
'CREATE TABLE "users" ("id" bigserial primary key, "email" character varying, "user_id" bigint, ' \
42+
'CONSTRAINT "fk_rails_6d0b8b3c2f" FOREIGN KEY ("user_id") REFERENCES "users" ("id") )',
43+
44+
# Create the index.
45+
'SET statement_timeout TO 0',
46+
"SET lock_timeout TO '30s'",
47+
'CREATE INDEX CONCURRENTLY "index_users_on_user_id" ON "users" ("user_id")',
48+
"SET lock_timeout TO '5s'",
49+
"SET statement_timeout TO '5s'",
50+
51+
"SET statement_timeout TO '70s'",
52+
], calls
53+
54+
run_migration(:down)
55+
refute @connection.table_exists?(:users)
56+
end
57+
58+
def test_create_table_idempotence
59+
# Simulates an interruption between the table creation and the index creation
60+
@connection.create_table(:users) do |t|
61+
t.string :name, index: true
62+
t.string :email
63+
end
64+
65+
@migration =
66+
Class.new(ActiveRecord::Migration::Current) do
67+
def change
68+
create_table(:users) do |t|
69+
t.string :name, index: true
70+
t.string :email, index: true
71+
end
72+
end
73+
end.new
74+
75+
calls = record_calls(@connection, :execute) { run_migration }
76+
indexes = ActiveRecord::Base.connection.indexes :users
77+
refute_empty indexes
78+
assert_equal 'index_users_on_email', indexes.first.name
79+
80+
refute_includes flat_calls(calls), 'CREATE INDEX CONCURRENTLY "index_users_on_name" ON "users" ("name")'
81+
82+
assert_calls [
83+
"SET statement_timeout TO '5s'",
84+
'SET statement_timeout TO 0',
85+
"SET lock_timeout TO '30s'",
86+
"SET lock_timeout TO '5s'",
87+
"SET statement_timeout TO '5s'",
88+
'SET statement_timeout TO 0',
89+
"SET lock_timeout TO '30s'",
90+
'CREATE INDEX CONCURRENTLY "index_users_on_email" ON "users" ("email")',
91+
"SET lock_timeout TO '5s'",
92+
"SET statement_timeout TO '5s'",
93+
"SET statement_timeout TO '70s'",
94+
], calls
95+
end
96+
end

test/safe_pg_migrations_test.rb

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,9 @@ def change
209209
assert_equal [
210210
'== 8128 : migrating ===========================================================',
211211
'-- create_table(:users)',
212-
" -> /!\\ Table 'users' already exists. Skipping statement.",
213-
], write_calls[0...3]
212+
" -> /!\\ Table 'users' already exists.",
213+
' -> -- Skipping statement',
214+
], write_calls[0...4]
214215
end
215216

216217
def test_add_column_idem_potent
@@ -319,39 +320,6 @@ def change
319320
refute @connection.column_exists?(:users, :user)
320321
end
321322

322-
def test_create_table
323-
@migration =
324-
Class.new(ActiveRecord::Migration::Current) do
325-
def change
326-
create_table(:users) do |t|
327-
t.string :email
328-
t.references :user, foreign_key: true
329-
end
330-
end
331-
end.new
332-
333-
calls = record_calls(@connection, :execute) { run_migration }
334-
assert_calls [
335-
"SET statement_timeout TO '5s'",
336-
337-
# Create the table with constraints.
338-
'CREATE TABLE "users" ("id" bigserial primary key, "email" character varying, "user_id" bigint, ' \
339-
'CONSTRAINT "fk_rails_6d0b8b3c2f" FOREIGN KEY ("user_id") REFERENCES "users" ("id") )',
340-
341-
# Create the index.
342-
'SET statement_timeout TO 0',
343-
"SET lock_timeout TO '30s'",
344-
'CREATE INDEX CONCURRENTLY "index_users_on_user_id" ON "users" ("user_id")',
345-
"SET lock_timeout TO '5s'",
346-
"SET statement_timeout TO '5s'",
347-
348-
"SET statement_timeout TO '70s'",
349-
], calls
350-
351-
run_migration(:down)
352-
refute @connection.table_exists?(:users)
353-
end
354-
355323
def test_add_index
356324
@connection.create_table(:users) { |t| t.string :email }
357325
@migration =

test/test_helper.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ def assert_calls(expected, actual)
3535
"SET lock_timeout TO '5s'",
3636
*expected,
3737
"SET lock_timeout TO '70s'",
38-
], actual.map(&:first).map(&:squish).reverse.drop_while { |call| %w[BEGIN COMMIT].include? call }.reverse
38+
], flat_calls(actual)
39+
end
40+
41+
def flat_calls(calls)
42+
calls.map(&:first).map(&:squish).reverse.drop_while { |call| %w[BEGIN COMMIT].include? call }.reverse
3943
end
4044

4145
# Records method calls on an object. Behaves like a test spy.

0 commit comments

Comments
 (0)