Skip to content

Commit 6aaf1df

Browse files
committed
Bigint Migration for 'events' Table (Step 3)
1 parent d752fc2 commit 6aaf1df

8 files changed

+363
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'database/bigint_migration'
2+
3+
Sequel.migration do
4+
up do
5+
if database_type == :postgres && !VCAP::BigintMigration.migration_completed?(self, :events) && !VCAP::BigintMigration.migration_skipped?(self, :events)
6+
begin
7+
VCAP::BigintMigration.add_check_constraint(self, :events)
8+
rescue Sequel::CheckConstraintViolation
9+
raise "Failed to add check constraint on 'events' table!\n" \
10+
"There are rows where 'id_bigint' does not match 'id', thus step 3 of the bigint migration cannot be executed.\n" \
11+
"Consider running rake task 'db:bigint_backfill[events]'."
12+
end
13+
end
14+
end
15+
16+
down do
17+
if database_type == :postgres && !VCAP::BigintMigration.migration_completed?(self, :events) && !VCAP::BigintMigration.migration_skipped?(self, :events)
18+
VCAP::BigintMigration.drop_check_constraint(self, :events)
19+
end
20+
end
21+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'database/bigint_migration'
2+
3+
Sequel.migration do
4+
up do
5+
if database_type == :postgres && !VCAP::BigintMigration.migration_completed?(self, :events) && !VCAP::BigintMigration.migration_skipped?(self, :events)
6+
transaction do
7+
VCAP::BigintMigration.drop_check_constraint(self, :events)
8+
VCAP::BigintMigration.drop_trigger_function(self, :events)
9+
VCAP::BigintMigration.drop_pk_id_column(self, :events)
10+
# ...
11+
end
12+
end
13+
end
14+
15+
down do
16+
if database_type == :postgres && !VCAP::BigintMigration.migration_completed?(self, :events) && !VCAP::BigintMigration.migration_skipped?(self, :events)
17+
VCAP::BigintMigration.add_pk_id_column(self, :events)
18+
VCAP::BigintMigration.create_trigger_function(self, :events)
19+
VCAP::BigintMigration.backfill_id(self, :events)
20+
VCAP::BigintMigration.add_check_constraint(self, :events)
21+
# ...
22+
end
23+
end
24+
end

lib/database/bigint_migration.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,56 @@ def backfill(logger, db, table, batch_size: 10_000, iterations: -1)
6262
logger.info("finished bigint backfill on table '#{table}'")
6363
end
6464

65+
def migration_completed?(db, table)
66+
column_type(db, table, :id) == 'bigint'
67+
end
68+
69+
def migration_skipped?(db, table)
70+
!column_exists?(db, table, :id_bigint)
71+
end
72+
73+
def add_check_constraint(db, table)
74+
return if check_constraint_exists?(db, table, :check_id_bigint_matches_id)
75+
76+
db.alter_table(table) do
77+
add_constraint(:check_id_bigint_matches_id) do
78+
Sequel.lit('id_bigint IS NOT NULL AND id_bigint = id')
79+
end
80+
end
81+
end
82+
83+
def drop_check_constraint(db, table)
84+
return unless check_constraint_exists?(db, table, :check_id_bigint_matches_id)
85+
86+
db.alter_table(table) do
87+
drop_constraint(:check_id_bigint_matches_id)
88+
end
89+
end
90+
91+
def drop_pk_id_column(db, table)
92+
db.drop_column(table, :id, if_exists: true)
93+
end
94+
95+
def add_pk_id_column(db, table)
96+
return if column_exists?(db, table, :id)
97+
98+
db.alter_table(table) do
99+
add_primary_key(:id)
100+
end
101+
end
102+
103+
def backfill_id(db, table)
104+
batch_size = 10_000
105+
loop do
106+
updated_rows = db.
107+
from(table, :batch).
108+
with(:batch, db[table].select(:id_bigint).where(id: nil).order(:id_bigint).limit(batch_size).for_update.skip_locked).
109+
where(Sequel.qualify(table, :id_bigint) => :batch__id_bigint).
110+
update(id: :batch__id_bigint)
111+
break if updated_rows < batch_size
112+
end
113+
end
114+
65115
private
66116

67117
def column_type(db, table, column)
@@ -79,5 +129,17 @@ def trigger_name(table)
79129
def column_exists?(db, table, column)
80130
db[table].columns.include?(column)
81131
end
132+
133+
def check_constraint_exists?(db, table, constraint_name)
134+
db.check_constraints(table).include?(constraint_name)
135+
end
136+
137+
def backfill_batch(db, table, from_column, to_column, batch_size)
138+
db.
139+
from(table, :batch).
140+
with(:batch, db[table].select(from_column).where(to_column => nil).order(from_column).limit(batch_size).for_update.skip_locked).
141+
where(Sequel.qualify(table, from_column) => :"batch__#{from_column}").
142+
update(to_column => :"batch__#{from_column}")
143+
end
82144
end
83145
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/bigint_migration_step3_shared_context'
3+
4+
RSpec.describe 'bigint migration - events table - step3a', isolation: :truncation, type: :migration do
5+
include_context 'bigint migration step3a' do
6+
let(:migration_filename_step1) { '20250327142351_bigint_migration_events_step1.rb' }
7+
let(:migration_filename_step3a) { '20250603103400_bigint_migration_events_step3a.rb' }
8+
let(:table) { :events }
9+
let(:insert) do
10+
lambda do |db|
11+
db[:events].insert(guid: SecureRandom.uuid, timestamp: Time.now.utc, type: 'type',
12+
actor: 'actor', actor_type: 'actor_type',
13+
actee: 'actee', actee_type: 'actee_type')
14+
end
15+
end
16+
end
17+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/bigint_migration_step3_shared_context'
3+
4+
RSpec.describe 'bigint migration - events table - step3b', isolation: :truncation, type: :migration do
5+
include_context 'bigint migration step3b' do
6+
let(:migration_filename_step1) { '20250327142351_bigint_migration_events_step1.rb' }
7+
let(:migration_filename_step3a) { '20250603103400_bigint_migration_events_step3a.rb' }
8+
let(:migration_filename_step3b) { '20250603103500_bigint_migration_events_step3b.rb' }
9+
let(:table) { :events }
10+
let(:insert) do
11+
lambda do |db|
12+
db[:events].insert(guid: SecureRandom.uuid, timestamp: Time.now.utc, type: 'type',
13+
actor: 'actor', actor_type: 'actor_type',
14+
actee: 'actee', actee_type: 'actee_type')
15+
end
16+
end
17+
end
18+
end

spec/migrations/helpers/bigint_migration_step1_shared_context.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060
context 'when the table is not empty' do
6161
let!(:old_id) { insert.call(db) }
6262

63+
after do
64+
db[table].delete # Necessary to successfully run subsequent migrations in the after block of the migration shared context...
65+
end
66+
6367
it "does not change the id column's type" do
6468
expect(db).to have_table_with_column_and_type(table, :id, 'integer')
6569

@@ -186,6 +190,10 @@
186190
run_migration
187191
end
188192

193+
after do
194+
db[table].delete # Necessary to successfully run subsequent migrations in the after block of the migration shared context...
195+
end
196+
189197
it 'drops the id_bigint column' do
190198
expect(db).to have_table_with_column(table, :id_bigint)
191199

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
require 'migrations/helpers/migration_shared_context'
2+
require 'database/bigint_migration'
3+
4+
RSpec.shared_context 'bigint migration step3a' do
5+
subject(:run_migration_step1) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }
6+
7+
subject(:run_migration_step3a) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index_step3a, allow_missing_migration_files: true) }
8+
9+
let(:migration_filename) { migration_filename_step1 }
10+
let(:current_migration_index_step3a) { migration_filename_step3a.match(/\A\d+/)[0].to_i }
11+
12+
include_context 'migration'
13+
14+
let(:skip_bigint_id_migration) { false }
15+
let(:logger) { double(:logger, info: nil) }
16+
17+
before do
18+
skip unless db.database_type == :postgres
19+
20+
allow_any_instance_of(VCAP::CloudController::Config).to receive(:get).with(:skip_bigint_id_migration).and_return(skip_bigint_id_migration)
21+
end
22+
23+
describe 'up' do
24+
context 'when migration step 1 was executed' do
25+
context 'when the id_bigint column was added' do
26+
before do
27+
insert.call(db)
28+
run_migration_step1
29+
end
30+
31+
context 'when backfilling was completed' do
32+
before do
33+
VCAP::BigintMigration.backfill(logger, db, table)
34+
end
35+
36+
it 'adds a check constraint' do
37+
expect(db).not_to have_table_with_check_constraint(table)
38+
39+
run_migration_step3a
40+
41+
expect(db).to have_table_with_check_constraint(table)
42+
end
43+
end
44+
45+
context 'when backfilling was not completed' do
46+
after do
47+
db[table].delete # Necessary as the migration will be executed again in the after block of the migration shared context - and should not fail...
48+
end
49+
50+
it 'fails ...' do
51+
expect do
52+
run_migration_step3a
53+
end.to raise_error(/Failed to add check constraint on 'events' table!/)
54+
end
55+
end
56+
end
57+
58+
context "when the migration was concluded (id column's type switched)" do
59+
before do
60+
db[table].delete
61+
run_migration_step1
62+
end
63+
64+
it 'does not add a check constraint' do
65+
expect(db).not_to have_table_with_check_constraint(table)
66+
67+
run_migration_step3a
68+
69+
expect(db).not_to have_table_with_check_constraint(table)
70+
end
71+
end
72+
end
73+
74+
context 'when migration step 1 was skipped' do
75+
let(:skip_bigint_id_migration) { true }
76+
77+
before do
78+
run_migration_step1
79+
end
80+
81+
it 'does not add a check constraint' do
82+
expect(db).not_to have_table_with_check_constraint(table)
83+
84+
run_migration_step3a
85+
86+
expect(db).not_to have_table_with_check_constraint(table)
87+
end
88+
end
89+
end
90+
91+
describe 'down' do
92+
subject(:run_rollback_step3a) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index_step3a - 1, allow_missing_migration_files: true) }
93+
94+
context 'when migration step 3a was executed' do
95+
before do
96+
insert.call(db)
97+
run_migration_step1
98+
VCAP::BigintMigration.backfill(logger, db, table)
99+
run_migration_step3a
100+
end
101+
102+
it 'drops the check constraint' do
103+
expect(db).to have_table_with_check_constraint(table)
104+
105+
run_rollback_step3a
106+
107+
expect(db).not_to have_table_with_check_constraint(table)
108+
end
109+
end
110+
end
111+
end
112+
113+
RSpec.shared_context 'bigint migration step3b' do
114+
subject(:run_migration_step1) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }
115+
116+
subject(:run_migration_step3a) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index_step3a, allow_missing_migration_files: true) }
117+
118+
subject(:run_migration_step3b) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index_step3b, allow_missing_migration_files: true) }
119+
120+
let(:migration_filename) { migration_filename_step1 }
121+
let(:current_migration_index_step3a) { migration_filename_step3a.match(/\A\d+/)[0].to_i }
122+
let(:current_migration_index_step3b) { migration_filename_step3b.match(/\A\d+/)[0].to_i }
123+
124+
include_context 'migration'
125+
126+
let(:skip_bigint_id_migration) { false }
127+
let(:logger) { double(:logger, info: nil) }
128+
129+
before do
130+
skip unless db.database_type == :postgres
131+
132+
allow_any_instance_of(VCAP::CloudController::Config).to receive(:get).with(:skip_bigint_id_migration).and_return(skip_bigint_id_migration)
133+
end
134+
135+
describe 'up' do
136+
context 'when migration step 3a was executed' do
137+
before do
138+
insert.call(db)
139+
run_migration_step1
140+
VCAP::BigintMigration.backfill(logger, db, table)
141+
run_migration_step3a
142+
end
143+
144+
it 'drops the check constraint' do
145+
expect(db).to have_table_with_check_constraint(table)
146+
147+
run_migration_step3b
148+
149+
expect(db).not_to have_table_with_check_constraint(table)
150+
end
151+
152+
it 'drops the trigger function' do
153+
expect(db).to have_trigger_function_for_table(table)
154+
155+
run_migration_step3b
156+
157+
expect(db).not_to have_trigger_function_for_table(table)
158+
end
159+
160+
it 'drops the id primary key column' do
161+
expect(db).to have_table_with_column(table, :id)
162+
163+
run_migration_step3b
164+
165+
expect(db).not_to have_table_with_column(table, :id)
166+
end
167+
end
168+
end
169+
170+
describe 'down' do
171+
subject(:run_rollback_step3b) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index_step3b - 1, allow_missing_migration_files: true) }
172+
173+
context 'when migration step 3b was executed' do
174+
before do
175+
insert.call(db)
176+
run_migration_step1
177+
VCAP::BigintMigration.backfill(logger, db, table)
178+
run_migration_step3a
179+
run_migration_step3b
180+
end
181+
182+
it 're-adds the id primary key column' do
183+
expect(db).not_to have_table_with_column(table, :id)
184+
185+
run_rollback_step3b
186+
187+
expect(db).to have_table_with_column(table, :id)
188+
end
189+
190+
it 're-creates the trigger function' do
191+
expect(db).not_to have_trigger_function_for_table(table)
192+
193+
run_rollback_step3b
194+
195+
expect(db).to have_trigger_function_for_table(table)
196+
end
197+
198+
it 're-adds the check constraint' do
199+
expect(db).not_to have_table_with_check_constraint(table)
200+
201+
run_rollback_step3b
202+
203+
expect(db).to have_table_with_check_constraint(table)
204+
end
205+
end
206+
end
207+
end

spec/migrations/helpers/matchers.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,9 @@
5656
db[table].where(column => nil).any?
5757
end
5858
end
59+
60+
RSpec::Matchers.define :have_table_with_check_constraint do |table|
61+
match do |db|
62+
db.check_constraints(table).include?(:check_id_bigint_matches_id)
63+
end
64+
end

0 commit comments

Comments
 (0)