Skip to content

Commit e8e1df1

Browse files
committed
Bigint Migration for 'events' Table (Step 3)
1 parent 0e57b51 commit e8e1df1

9 files changed

+542
-7
lines changed

db/migrations/20250327142351_bigint_migration_events_step1.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@
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.
21+
#
22+
# Ideally this down migration SHOULD NEVER BE EXECUTED IN A PRODUCTION SYSTEM! (It's there for proper integration
23+
# testing of the bigint migration steps.)
1724
VCAP::BigintMigration.revert_pk_to_integer(self, :events)
25+
1826
VCAP::BigintMigration.drop_trigger_function(self, :events)
1927
VCAP::BigintMigration.drop_bigint_column(self, :events)
2028
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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
# Drop check constraint and trigger function
7+
VCAP::BigintMigration.drop_check_constraint(self, :events)
8+
VCAP::BigintMigration.drop_trigger_function(self, :events)
9+
10+
# Drop old id column
11+
VCAP::BigintMigration.drop_pk_column(self, :events)
12+
13+
# Switch id_bigint -> id
14+
VCAP::BigintMigration.rename_bigint_column(self, :events)
15+
VCAP::BigintMigration.add_pk_constraint(self, :events)
16+
VCAP::BigintMigration.add_timestamp_pk_index(self, :events)
17+
VCAP::BigintMigration.set_pk_as_identity_with_correct_start_value(self, :events)
18+
end
19+
end
20+
21+
down do
22+
if database_type == :postgres && VCAP::BigintMigration.migration_completed?(self, :events)
23+
# Revert id -> id_bigint
24+
VCAP::BigintMigration.drop_identity(self, :events)
25+
VCAP::BigintMigration.drop_timestamp_pk_index(self, :events)
26+
VCAP::BigintMigration.drop_pk_constraint(self, :events)
27+
VCAP::BigintMigration.revert_bigint_column_name(self, :events)
28+
29+
# Restore old id column
30+
VCAP::BigintMigration.add_id_column(self, :events)
31+
32+
# To restore the previous state it is necessary to backfill the id column. In case there is a lot of data in the
33+
# table this might be problematic, e.g. take a longer time.
34+
#
35+
# Ideally this down migration SHOULD NEVER BE EXECUTED IN A PRODUCTION SYSTEM! (It's there for proper integration
36+
# testing of the bigint migration steps.)
37+
VCAP::BigintMigration.backfill_id(self, :events)
38+
39+
VCAP::BigintMigration.add_pk_constraint(self, :events)
40+
VCAP::BigintMigration.add_timestamp_pk_index(self, :events)
41+
VCAP::BigintMigration.set_pk_as_identity_with_correct_start_value(self, :events)
42+
43+
# Recreate trigger function and check constraint
44+
VCAP::BigintMigration.create_trigger_function(self, :events)
45+
VCAP::BigintMigration.add_check_constraint(self, :events)
46+
end
47+
end
48+
end

lib/database/bigint_migration.rb

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,124 @@ 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+
constraint_name = check_constraint_name(table)
73+
db.alter_table(table) do
74+
add_constraint(constraint_name) do
75+
Sequel.lit('id_bigint IS NOT NULL AND id_bigint = id')
76+
end
77+
end
78+
end
79+
80+
def drop_check_constraint(db, table)
81+
return unless has_check_constraint?(db, table)
82+
83+
constraint_name = check_constraint_name(table)
84+
db.alter_table(table) do
85+
drop_constraint(constraint_name)
86+
end
87+
end
88+
89+
def has_check_constraint?(db, table)
90+
check_constraint_exists?(db, table, check_constraint_name(table))
91+
end
92+
93+
def drop_pk_column(db, table)
94+
db.drop_column(table, :id, if_exists: true)
95+
end
96+
97+
def add_id_column(db, table)
98+
db.add_column(table, :id, :integer, if_not_exists: true)
99+
end
100+
101+
def rename_bigint_column(db, table)
102+
db.rename_column(table, :id_bigint, :id) if column_exists?(db, table, :id_bigint) && !column_exists?(db, table, :id)
103+
end
104+
105+
def revert_bigint_column_name(db, table)
106+
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)
107+
end
108+
109+
def add_pk_constraint(db, table)
110+
return if db.primary_key(table) == 'id'
111+
112+
db.alter_table(table) do
113+
add_primary_key([:id])
114+
end
115+
end
116+
117+
def drop_pk_constraint(db, table)
118+
return unless db.primary_key(table) == 'id'
119+
120+
constraint_name = pk_constraint_name(table)
121+
db.alter_table(table) do
122+
drop_constraint(constraint_name)
123+
set_column_allow_null(:id, true)
124+
end
125+
end
126+
127+
def add_timestamp_pk_index(db, table)
128+
db.add_index(table, %i[timestamp id], name: timestamp_id_index_name(table), unique: false, if_not_exists: true)
129+
end
130+
131+
def drop_timestamp_pk_index(db, table)
132+
db.drop_index(table, %i[timestamp id], name: timestamp_id_index_name(table), if_exists: true)
133+
end
134+
135+
def set_pk_as_identity_with_correct_start_value(db, table)
136+
return if column_attribute(db, table, :id, :auto_increment) == true
137+
138+
block = <<~BLOCK
139+
DO $$
140+
DECLARE
141+
max_id BIGINT;
142+
BEGIN
143+
SELECT COALESCE(MAX(id), 0) + 1 INTO max_id FROM #{table};
144+
145+
EXECUTE format('ALTER TABLE #{table} ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (START WITH %s)', max_id);
146+
END $$;
147+
BLOCK
148+
db.run(block)
149+
end
150+
151+
def drop_identity(db, table)
152+
db.run("ALTER TABLE #{table} ALTER COLUMN id DROP IDENTITY IF EXISTS")
153+
end
154+
155+
def backfill_id(db, table)
156+
batch_size = 10_000
157+
loop do
158+
updated_rows = backfill_batch(db, table, :id_bigint, :id, batch_size)
159+
break if updated_rows < batch_size
160+
end
161+
end
162+
65163
private
66164

165+
def column_attribute(db, table, column, attribute)
166+
db.schema(table).find { |col, _| col == column }&.dig(1, attribute)
167+
end
168+
67169
def column_type(db, table, column)
68-
db.schema(table).find { |col, _| col == column }&.dig(1, :db_type)
170+
column_attribute(db, table, column, :db_type)
69171
end
70172

71173
def function_name(table)
@@ -79,5 +181,29 @@ def trigger_name(table)
79181
def column_exists?(db, table, column)
80182
db[table].columns.include?(column)
81183
end
184+
185+
def check_constraint_name(table)
186+
:"#{table}_check_id_bigint_matches_id"
187+
end
188+
189+
def check_constraint_exists?(db, table, constraint_name)
190+
db.check_constraints(table).include?(constraint_name)
191+
end
192+
193+
def pk_constraint_name(table)
194+
:"#{table}_pkey"
195+
end
196+
197+
def timestamp_id_index_name(table)
198+
:"#{table}_timestamp_id_index"
199+
end
200+
201+
def backfill_batch(db, table, from_column, to_column, batch_size)
202+
db.
203+
from(table, :batch).
204+
with(:batch, db[table].select(from_column).where(to_column => nil).order(from_column).limit(batch_size).for_update.skip_locked).
205+
where(Sequel.qualify(table, from_column) => :"batch__#{from_column}").
206+
update(to_column => :"batch__#{from_column}")
207+
end
82208
end
83209
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)