Skip to content

Commit 29d1456

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

8 files changed

+402
-0
lines changed
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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) # TODO: test
9+
VCAP::BigintMigration.rename_bigint_column(self, :events) # TODO: test
10+
VCAP::BigintMigration.add_pk_constraint(self, :events) # TODO: test
11+
VCAP::BigintMigration.set_pk_as_identity_with_correct_start_value(self, :events) # TODO: test
12+
end
13+
end
14+
15+
down do
16+
if database_type == :postgres
17+
if VCAP::BigintMigration.table_empty?(self, :events) # TODO: test
18+
VCAP::BigintMigration.revert_pk_to_integer(self, :events)
19+
else
20+
VCAP::BigintMigration.drop_identity(self, :events) # TODO: test
21+
VCAP::BigintMigration.drop_pk_constraint(self, :events) # TODO: test
22+
VCAP::BigintMigration.revert_bigint_column_name(self, :events) # TODO: test
23+
VCAP::BigintMigration.add_id_column(self, :events) # TODO: test
24+
VCAP::BigintMigration.backfill_id(self, :events) # TODO: test
25+
VCAP::BigintMigration.add_pk_constraint(self, :events) # TODO: test
26+
VCAP::BigintMigration.set_pk_as_identity_with_correct_start_value(self, :events) # TODO: test
27+
VCAP::BigintMigration.create_trigger_function(self, :events)
28+
VCAP::BigintMigration.add_check_constraint(self, :events)
29+
end
30+
end
31+
end
32+
end

lib/database/bigint_migration.rb

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,105 @@ 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 has_check_constraint?(db, table)
92+
check_constraint_exists?(db, table, :check_id_bigint_matches_id)
93+
end
94+
95+
def drop_pk_column(db, table)
96+
db.drop_column(table, :id, if_exists: true)
97+
end
98+
99+
def add_id_column(db, table)
100+
db.add_column(table, :id, :integer, if_not_exists: true)
101+
end
102+
103+
def rename_bigint_column(db, table)
104+
db.rename_column(table, :id_bigint, :id) if column_exists?(db, table, :id_bigint) && !column_exists?(db, table, :id)
105+
end
106+
107+
def revert_bigint_column_name(db, table)
108+
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)
109+
end
110+
111+
def add_pk_constraint(db, table)
112+
return if db.primary_key(table) == 'id'
113+
114+
db.alter_table(table) do
115+
add_primary_key([:id])
116+
end
117+
end
118+
119+
def drop_pk_constraint(db, table)
120+
return unless db.primary_key(table) == 'id'
121+
122+
db.alter_table(table) do
123+
drop_constraint(:"#{table}_pkey")
124+
end
125+
end
126+
127+
def set_pk_as_identity_with_correct_start_value(db, table)
128+
# TODO: ???
129+
db.fetch("SELECT * FROM information_schema.columns WHERE table_name = '#{table}' AND column_name = 'id' AND is_identity = 'YES';") do
130+
return
131+
end
132+
133+
db.run <<-SQL.squish
134+
DO $$
135+
DECLARE
136+
max_id BIGINT;
137+
BEGIN
138+
SELECT COALESCE(MAX(id), 0) + 1 INTO max_id FROM events;
139+
140+
EXECUTE format('ALTER TABLE #{table} ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (START WITH %s)', max_id);
141+
END $$;
142+
SQL
143+
144+
# TODO: ???
145+
# db.reset_primary_key_sequence(table)
146+
end
147+
148+
def drop_identity(db, table)
149+
db.run("ALTER TABLE #{table} ALTER COLUMN id DROP IDENTITY IF EXISTS")
150+
end
151+
152+
def backfill_id(db, table)
153+
batch_size = 10_000
154+
loop do
155+
updated_rows = db.
156+
from(table, :batch).
157+
with(:batch, db[table].select(:id_bigint).where(id: nil).order(:id_bigint).limit(batch_size).for_update.skip_locked).
158+
where(Sequel.qualify(table, :id_bigint) => :batch__id_bigint).
159+
update(id: :batch__id_bigint)
160+
break if updated_rows < batch_size
161+
end
162+
end
163+
65164
private
66165

67166
def column_type(db, table, column)
@@ -79,5 +178,17 @@ def trigger_name(table)
79178
def column_exists?(db, table, column)
80179
db[table].columns.include?(column)
81180
end
181+
182+
def check_constraint_exists?(db, table, constraint_name)
183+
db.check_constraints(table).include?(constraint_name)
184+
end
185+
186+
def backfill_batch(db, table, from_column, to_column, batch_size)
187+
db.
188+
from(table, :batch).
189+
with(:batch, db[table].select(from_column).where(to_column => nil).order(from_column).limit(batch_size).for_update.skip_locked).
190+
where(Sequel.qualify(table, from_column) => :"batch__#{from_column}").
191+
update(to_column => :"batch__#{from_column}")
192+
end
82193
end
83194
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)