Skip to content

Commit a2fd52c

Browse files
committed
bigint migration - events table - step 1
1 parent f629077 commit a2fd52c

File tree

9 files changed

+357
-2
lines changed

9 files changed

+357
-2
lines changed

.rubocop_cc.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ Rails/DangerousColumnNames: # Disabled, in comparison to active_record we need t
136136
Rails/SkipsModelValidations: # We don`t want any model at all in migrations and migration specs
137137
Enabled: true
138138
Exclude:
139-
- db/migrations/*
140-
- spec/migrations/*
139+
- db/migrations/**/*
140+
- spec/migrations/**/*
141141

142142
#### ENABLED SECTION
143143
Gemspec/DeprecatedAttributeAssignment:

db/helpers/bigint_migration.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# helpers
2+
def opt_out?
3+
opt_out = VCAP::CloudController::Config.config&.get(:skip_bigint_id_migration)
4+
opt_out.nil? ? false : opt_out
5+
rescue VCAP::CloudController::Config::InvalidConfigPath
6+
false
7+
end
8+
9+
# DSL
10+
def empty?(table)
11+
raise unless is_a?(Sequel::Database)
12+
13+
self[table].count == 0
14+
end
15+
16+
def change_pk_to_bigint(table)
17+
raise unless is_a?(Sequel::Database)
18+
19+
set_column_type(table, :id, :Bignum) if column_type(self, table, :id) != BIGINT_TYPE
20+
end
21+
22+
def add_bigint_column(table)
23+
raise unless is_a?(Sequel::Database)
24+
25+
add_column(table, :id_bigint, :Bignum) unless schema(table).map(&:first).include?(:id_bigint)
26+
end
27+
28+
def drop_trigger_function(table)
29+
raise unless is_a?(Sequel::Database)
30+
31+
if database_type == :postgres
32+
drop_trigger(table, trigger_name(table), if_exists: true)
33+
drop_function(function_name(table), if_exists: true)
34+
elsif database_type == :mysql
35+
run("DROP TRIGGER #{trigger_name(table)};")
36+
end
37+
end
38+
39+
def create_trigger_function(table)
40+
drop_trigger_function(table)
41+
42+
raise unless is_a?(Sequel::Database)
43+
44+
if database_type == :postgres
45+
function = <<~FUNC
46+
BEGIN
47+
NEW.id_bigint := NEW.id;
48+
RETURN NEW;
49+
END;
50+
FUNC
51+
create_function(function_name(table), function, language: :plpgsql, returns: :trigger)
52+
create_trigger(table, trigger_name(table), function_name(table), each_row: true, events: :insert)
53+
elsif database_type == :mysql
54+
run("CREATE TRIGGER #{trigger_name(table)} BEFORE INSERT ON #{table} FOR EACH ROW SET NEW.id_bigint = NEW.id;")
55+
end
56+
end
57+
58+
def revert_pk_to_integer(table)
59+
raise unless is_a?(Sequel::Database)
60+
61+
set_column_type(table, :id, :integer) if column_type(self, table, :id) == BIGINT_TYPE
62+
end
63+
64+
def drop_bigint_column(table)
65+
raise unless is_a?(Sequel::Database)
66+
67+
drop_column(table, :id_bigint) if schema(table).map(&:first).include?(:id_bigint)
68+
end
69+
70+
# internal constants
71+
BIGINT_TYPE = 'bigint'.freeze
72+
73+
# internal helpers
74+
def column_type(db, table, column)
75+
db.schema(table).find { |col, _| col == column }&.dig(1, :db_type)
76+
end
77+
78+
def function_name(table)
79+
:"#{table}_set_id_bigint_on_insert"
80+
end
81+
82+
def trigger_name(table)
83+
:"trigger_#{function_name(table)}"
84+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
require File.expand_path('../helpers/bigint_migration', __dir__)
2+
3+
Sequel.migration do
4+
up do
5+
unless opt_out?
6+
if empty?(:events)
7+
change_pk_to_bigint(:events)
8+
else
9+
add_bigint_column(:events)
10+
create_trigger_function(:events)
11+
end
12+
end
13+
end
14+
15+
down do
16+
revert_pk_to_integer(:events)
17+
drop_trigger_function(:events)
18+
drop_bigint_column(:events)
19+
end
20+
end

lib/cloud_controller/config_schemas/base/api_schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class ApiSchema < VCAP::Config
9999
optional(:max_migration_statement_runtime_in_seconds) => Integer,
100100
optional(:migration_psql_concurrent_statement_timeout_in_seconds) => Integer,
101101
optional(:migration_psql_worker_memory_kb) => Integer,
102+
optional(:skip_bigint_id_migration) => bool,
102103
db: {
103104
optional(:database) => Hash, # db connection hash for sequel
104105
max_connections: Integer, # max connections in the connection pool

lib/cloud_controller/config_schemas/base/migrate_schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class MigrateSchema < VCAP::Config
1010
optional(:max_migration_statement_runtime_in_seconds) => Integer,
1111
optional(:migration_psql_concurrent_statement_timeout_in_seconds) => Integer,
1212
optional(:migration_psql_worker_memory_kb) => Integer,
13+
optional(:skip_bigint_id_migration) => bool,
1314

1415
db: {
1516
optional(:database) => Hash, # db connection hash for sequel
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/bigint_migration_step1_shared_context'
3+
4+
RSpec.describe 'bigint migration - events table - step1', isolation: :truncation, type: :migration do
5+
include_context 'bigint migration step1' do
6+
let(:migration_filename) { '20250327142351_bigint_migration_events_step1.rb' }
7+
let(:table) { :events }
8+
let(:insert_hash) do
9+
{
10+
guid: 'guid', timestamp: Time.now.utc, type: 'type',
11+
actor: 'actor', actor_type: 'actor_type',
12+
actee: 'actee', actee_type: 'actee_type'
13+
}
14+
end
15+
end
16+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
require 'spec_helper'
2+
require_relative '../../db/helpers/bigint_migration'
3+
4+
RSpec.describe 'bigint migration helpers', isolation: :truncation, type: :migration do
5+
let(:skip_bigint_id_migration) { nil }
6+
7+
before do
8+
allow_any_instance_of(VCAP::CloudController::Config).to receive(:get).with(:skip_bigint_id_migration).and_return(skip_bigint_id_migration)
9+
end
10+
11+
describe '#opt_out?' do
12+
context 'when skip_bigint_id_migration is false' do
13+
let(:skip_bigint_id_migration) { false }
14+
15+
it 'returns false' do
16+
expect(opt_out?).to be(false)
17+
end
18+
end
19+
20+
context 'when skip_bigint_id_migration is true' do
21+
let(:skip_bigint_id_migration) { true }
22+
23+
it 'returns true' do
24+
expect(opt_out?).to be(true)
25+
end
26+
end
27+
28+
context 'when skip_bigint_id_migration is nil' do
29+
let(:skip_bigint_id_migration) { nil }
30+
31+
it 'returns false' do
32+
expect(opt_out?).to be(false)
33+
end
34+
end
35+
36+
context 'when raising InvalidConfigPath error' do
37+
before do
38+
allow_any_instance_of(VCAP::CloudController::Config).to receive(:get).with(:skip_bigint_id_migration).and_raise(VCAP::CloudController::Config::InvalidConfigPath)
39+
end
40+
41+
it 'returns false' do
42+
expect(opt_out?).to be(false)
43+
end
44+
end
45+
end
46+
end
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
require 'migrations/helpers/migration_shared_context'
2+
3+
RSpec.shared_context 'bigint migration step1' do
4+
subject(:run_migration) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }
5+
6+
include_context 'migration'
7+
8+
let(:skip_bigint_id_migration) { nil }
9+
10+
before do
11+
allow_any_instance_of(VCAP::CloudController::Config).to receive(:get).with(:skip_bigint_id_migration).and_return(skip_bigint_id_migration)
12+
end
13+
14+
describe 'up' do
15+
context 'when skip_bigint_id_migration is false' do
16+
let(:skip_bigint_id_migration) { false }
17+
18+
context 'when the table is empty' do
19+
before do
20+
db[table].delete
21+
end
22+
23+
it "changes the id column's type to bigint" do
24+
expect(db).to have_table_with_column_and_type(table, :id, :integer)
25+
26+
run_migration
27+
28+
expect(db).to have_table_with_column_and_type(table, :id, :bigint)
29+
end
30+
31+
it 'does not add the id_bigint column' do
32+
expect(db).not_to have_table_with_column(table, :id_bigint)
33+
34+
run_migration
35+
36+
expect(db).not_to have_table_with_column(table, :id_bigint)
37+
end
38+
end
39+
40+
context 'when the table is not empty' do
41+
before do
42+
db[table].insert(insert_hash)
43+
end
44+
45+
it "does not change the id column's type" do
46+
expect(db).to have_table_with_column_and_type(table, :id, :integer)
47+
48+
run_migration
49+
50+
expect(db).to have_table_with_column_and_type(table, :id, :integer)
51+
end
52+
53+
it 'adds the id_bigint column' do
54+
expect(db).not_to have_table_with_column(table, :id_bigint)
55+
56+
run_migration
57+
58+
expect(db).to have_table_with_column_and_type(table, :id_bigint, :bigint)
59+
end
60+
61+
it 'creates the trigger function' do
62+
expect(db).not_to have_trigger_function_for_table(table) unless db.database_type == :mysql
63+
64+
run_migration
65+
66+
expect(db).to have_trigger_function_for_table(table) unless db.database_type == :mysql
67+
end
68+
end
69+
end
70+
71+
context 'when skip_bigint_id_migration is true' do
72+
let(:skip_bigint_id_migration) { true }
73+
74+
it "neither changes the id column's type, nor adds the id_bigint column" do
75+
expect(db).to have_table_with_column_and_type(table, :id, :integer)
76+
expect(db).not_to have_table_with_column(table, :id_bigint)
77+
78+
run_migration
79+
80+
expect(db).to have_table_with_column_and_type(table, :id, :integer)
81+
expect(db).not_to have_table_with_column(table, :id_bigint)
82+
end
83+
end
84+
end
85+
86+
describe 'down' do
87+
subject(:run_rollback) { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }
88+
89+
context 'when the table is empty' do
90+
before do
91+
db[table].delete
92+
run_migration
93+
end
94+
95+
it "reverts the id column's type to integer" do
96+
expect(db).to have_table_with_column_and_type(table, :id, :bigint)
97+
98+
run_rollback
99+
100+
expect(db).to have_table_with_column_and_type(table, :id, :integer)
101+
end
102+
end
103+
104+
context 'when the table is not empty' do
105+
before do
106+
db[table].insert(insert_hash)
107+
run_migration
108+
end
109+
110+
it 'drops the id_bigint column' do
111+
expect(db).to have_table_with_column(table, :id_bigint)
112+
113+
run_rollback
114+
115+
expect(db).not_to have_table_with_column(table, :id_bigint)
116+
end
117+
118+
it 'drops the trigger function' do
119+
expect(db).to have_trigger_function_for_table(table) unless db.database_type == :mysql
120+
121+
run_rollback
122+
123+
expect(db).not_to have_trigger_function_for_table(table) unless db.database_type == :mysql
124+
end
125+
end
126+
end
127+
end

spec/migrations/helpers/matchers.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,63 @@
1515
!options.delete(:name).nil? && (options - %i[if_exists concurrently]).empty?
1616
end
1717
end
18+
19+
RSpec::Matchers.define :have_table_with_column do |table, column|
20+
match do |db|
21+
db[table].columns.include?(column)
22+
end
23+
end
24+
25+
RSpec::Matchers.define :have_table_with_column_and_type do |table, column, type|
26+
match do |db|
27+
expect(db).to have_table_with_column(table, column)
28+
29+
db.schema(table).find { |col, _| col == column }&.dig(1, :db_type) == db_type(db, type)
30+
end
31+
end
32+
33+
def db_type(db, type)
34+
case type
35+
when :integer
36+
if db.database_type == :postgres
37+
'integer'
38+
elsif db.database_type == :mysql
39+
'int'
40+
else
41+
raise "unsupported database type: #{db.database_type}"
42+
end
43+
when :bigint
44+
'bigint'
45+
else
46+
raise "unsupported type: #{type}"
47+
end
48+
end
49+
50+
RSpec::Matchers.define :have_trigger_function_for_table do |table|
51+
match do |db|
52+
function_name = :"#{table}_set_id_bigint_on_insert"
53+
trigger_name = :"trigger_#{function_name}"
54+
55+
if db.database_type == :postgres
56+
function_exists = false
57+
begin
58+
db.fetch("select pg_get_functiondef('#{function_name}()'::regprocedure);") do
59+
function_exists = true
60+
end
61+
rescue Sequel::DatabaseError => e
62+
raise 'unexpected database error' unless e.message =~ /PG::UndefinedFunction/
63+
end
64+
65+
trigger_exists = false
66+
db.fetch("select 1 as one from information_schema.triggers WHERE trigger_name = '#{trigger_name}' limit 1;") do
67+
trigger_exists = true
68+
end
69+
70+
raise 'either function and trigger must exist or none of them' if function_exists != trigger_exists
71+
72+
function_exists && trigger_exists
73+
elsif db.database_type == :mysql
74+
# TODO
75+
end
76+
end
77+
end

0 commit comments

Comments
 (0)