Skip to content

Commit 5ef5cc8

Browse files
committed
Bigint Migration for 'events' Table (Step 3)
1 parent ea840bb commit 5ef5cc8

9 files changed

+460
-5
lines changed

db/migrations/20250327142351_bigint_migration_events_step1.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
down do
1616
if database_type == :postgres
17+
# There is no guarantee that the table is still empty - which was the condition for simply switching the id
18+
# column's type to bigint. We nevertheless want to revert the type to integer as this is the opposite procedure of
19+
# the up migration. In case there is a lot of data in the table at this moment in time, this change might be
20+
# problematic, e.g. take a longer time.
1721
VCAP::BigintMigration.revert_pk_to_integer(self, :events)
22+
1823
VCAP::BigintMigration.drop_trigger_function(self, :events)
1924
VCAP::BigintMigration.drop_bigint_column(self, :events)
2025
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
VCAP::BigintMigration.drop_check_constraint(self, :events) if database_type == :postgres
18+
end
19+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'database/bigint_migration'
2+
3+
Sequel.migration do
4+
up do
5+
if database_type == :postgres && VCAP::BigintMigration.has_check_constraint?(self, :events)
6+
VCAP::BigintMigration.drop_check_constraint(self, :events)
7+
VCAP::BigintMigration.drop_trigger_function(self, :events)
8+
VCAP::BigintMigration.drop_pk_column(self, :events)
9+
VCAP::BigintMigration.rename_bigint_column(self, :events)
10+
VCAP::BigintMigration.add_pk_constraint(self, :events)
11+
VCAP::BigintMigration.set_pk_as_identity_with_correct_start_value(self, :events)
12+
end
13+
end
14+
15+
down do
16+
if database_type == :postgres && VCAP::BigintMigration.migration_completed?(self, :events)
17+
VCAP::BigintMigration.drop_identity(self, :events)
18+
VCAP::BigintMigration.drop_pk_constraint(self, :events)
19+
VCAP::BigintMigration.revert_bigint_column_name(self, :events)
20+
VCAP::BigintMigration.add_id_column(self, :events)
21+
# TODO: comment
22+
VCAP::BigintMigration.backfill_id(self, :events)
23+
VCAP::BigintMigration.add_pk_constraint(self, :events)
24+
VCAP::BigintMigration.set_pk_as_identity_with_correct_start_value(self, :events)
25+
VCAP::BigintMigration.create_trigger_function(self, :events)
26+
VCAP::BigintMigration.add_check_constraint(self, :events)
27+
end
28+
end
29+
end

lib/database/bigint_migration.rb

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,108 @@ def backfill(logger, db, table, batch_size: 10_000, iterations: -1)
5050

5151
logger.info("starting bigint backfill on table '#{table}' (batch_size: #{batch_size}, iterations: #{iterations})")
5252
loop do
53-
updated_rows = db.
54-
from(table, :batch).
55-
with(:batch, db[table].select(:id).where(id_bigint: nil).order(:id).limit(batch_size).for_update.skip_locked).
56-
where(Sequel.qualify(table, :id) => :batch__id).
57-
update(id_bigint: :batch__id)
53+
updated_rows = backfill_batch(db, table, :id, :id_bigint, batch_size)
5854
logger.info("updated #{updated_rows} rows")
5955
iterations -= 1 if iterations > 0
6056
break if updated_rows < batch_size || iterations == 0
6157
end
6258
logger.info("finished bigint backfill on table '#{table}'")
6359
end
6460

61+
def migration_completed?(db, table)
62+
column_type(db, table, :id) == 'bigint'
63+
end
64+
65+
def migration_skipped?(db, table)
66+
!column_exists?(db, table, :id_bigint)
67+
end
68+
69+
def add_check_constraint(db, table)
70+
return if has_check_constraint?(db, table)
71+
72+
db.alter_table(table) do
73+
add_constraint(:check_id_bigint_matches_id) do
74+
Sequel.lit('id_bigint IS NOT NULL AND id_bigint = id')
75+
end
76+
end
77+
end
78+
79+
def drop_check_constraint(db, table)
80+
return unless has_check_constraint?(db, table)
81+
82+
db.alter_table(table) do
83+
drop_constraint(:check_id_bigint_matches_id)
84+
end
85+
end
86+
87+
def has_check_constraint?(db, table)
88+
check_constraint_exists?(db, table, :check_id_bigint_matches_id)
89+
end
90+
91+
def drop_pk_column(db, table)
92+
db.drop_column(table, :id, if_exists: true)
93+
end
94+
95+
def add_id_column(db, table)
96+
db.add_column(table, :id, :integer, if_not_exists: true)
97+
end
98+
99+
def rename_bigint_column(db, table)
100+
db.rename_column(table, :id_bigint, :id) if column_exists?(db, table, :id_bigint) && !column_exists?(db, table, :id)
101+
end
102+
103+
def revert_bigint_column_name(db, table)
104+
db.rename_column(table, :id, :id_bigint) if column_exists?(db, table, :id) && column_type(db, table, :id) == 'bigint' && !column_exists?(db, table, :id_bigint)
105+
end
106+
107+
def add_pk_constraint(db, table)
108+
return if db.primary_key(table) == 'id'
109+
110+
db.alter_table(table) do
111+
add_primary_key([:id])
112+
end
113+
end
114+
115+
def drop_pk_constraint(db, table)
116+
return unless db.primary_key(table) == 'id'
117+
118+
db.alter_table(table) do
119+
drop_constraint(:"#{table}_pkey")
120+
end
121+
end
122+
123+
def set_pk_as_identity_with_correct_start_value(db, table)
124+
# TODO: ???
125+
db.fetch("SELECT * FROM information_schema.columns WHERE table_name = '#{table}' AND column_name = 'id' AND is_identity = 'YES';") do
126+
return
127+
end
128+
129+
block = <<~BLOCK
130+
DO $$
131+
DECLARE
132+
max_id BIGINT;
133+
BEGIN
134+
SELECT COALESCE(MAX(id), 0) + 1 INTO max_id FROM events;
135+
136+
EXECUTE format('ALTER TABLE #{table} ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (START WITH %s)', max_id);
137+
END $$;
138+
BLOCK
139+
db.run(block)
140+
end
141+
142+
def drop_identity(db, table)
143+
# TODO: ???
144+
db.run("ALTER TABLE #{table} ALTER COLUMN id DROP IDENTITY IF EXISTS")
145+
end
146+
147+
def backfill_id(db, table)
148+
batch_size = 10_000
149+
loop do
150+
updated_rows = backfill_batch(db, table, :id_bigint, :id, batch_size)
151+
break if updated_rows < batch_size
152+
end
153+
end
154+
65155
private
66156

67157
def column_type(db, table, column)
@@ -79,5 +169,17 @@ def trigger_name(table)
79169
def column_exists?(db, table, column)
80170
db[table].columns.include?(column)
81171
end
172+
173+
def check_constraint_exists?(db, table, constraint_name)
174+
db.check_constraints(table).include?(constraint_name)
175+
end
176+
177+
def backfill_batch(db, table, from_column, to_column, batch_size)
178+
db.
179+
from(table, :batch).
180+
with(:batch, db[table].select(from_column).where(to_column => nil).order(from_column).limit(batch_size).for_update.skip_locked).
181+
where(Sequel.qualify(table, from_column) => :"batch__#{from_column}").
182+
update(to_column => :"batch__#{from_column}")
183+
end
82184
end
83185
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

0 commit comments

Comments
 (0)