discourse/spec/components/migration/column_dropper_spec.rb
Sam 5f64fd0a21 DEV: remove exec_sql and replace with mini_sql
Introduce new patterns for direct sql that are safe and fast.

MiniSql is not prone to memory bloat that can happen with direct PG usage.
It also has an extremely fast materializer and very a convenient API

- DB.exec(sql, *params) => runs sql returns row count
- DB.query(sql, *params) => runs sql returns usable objects (not a hash)
- DB.query_hash(sql, *params) => runs sql returns an array of hashes
- DB.query_single(sql, *params) => runs sql and returns a flat one dimensional array
- DB.build(sql) => returns a sql builder

See more at: https://github.com/discourse/mini_sql
2018-06-19 16:13:36 +10:00

198 lines
5.3 KiB
Ruby

require 'rails_helper'
require_dependency 'migration/column_dropper'
RSpec.describe Migration::ColumnDropper do
def has_column?(table, column)
DB.exec(<<~SQL, table: table, column: column) == 1
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = 'public' AND
table_name = :table AND
column_name = :column
SQL
end
def update_first_migration_date(created_at)
DB.exec(<<~SQL, created_at: created_at)
UPDATE schema_migration_details
SET created_at = :created_at
WHERE id = (SELECT MIN(id)
FROM schema_migration_details)
SQL
end
describe ".drop" do
let(:migration_name) do
DB.query_single("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1").first
end
before do
DB.exec "ALTER TABLE topics ADD COLUMN junk int"
DB.exec(<<~SQL, name: migration_name, created_at: 15.minutes.ago)
UPDATE schema_migration_details
SET created_at = :created_at
WHERE name = :name
SQL
end
it "can correctly drop columns after correct delay" do
dropped_proc_called = false
update_first_migration_date(2.years.ago)
Migration::ColumnDropper.drop(
table: 'topics',
after_migration: migration_name,
columns: ['junk'],
delay: 20.minutes,
on_drop: ->() { dropped_proc_called = true }
)
expect(has_column?('topics', 'junk')).to eq(true)
expect(dropped_proc_called).to eq(false)
Migration::ColumnDropper.drop(
table: 'topics',
after_migration: migration_name,
columns: ['junk'],
delay: 10.minutes,
on_drop: ->() { dropped_proc_called = true }
)
expect(has_column?('topics', 'junk')).to eq(false)
expect(dropped_proc_called).to eq(true)
dropped_proc_called = false
Migration::ColumnDropper.drop(
table: 'topics',
after_migration: migration_name,
columns: ['junk'],
delay: 10.minutes,
on_drop: ->() { dropped_proc_called = true }
)
# it should call "on_drop" only when there are columns to drop
expect(dropped_proc_called).to eq(false)
end
it "drops the columns immediately if the first migration was less than 10 minutes ago" do
dropped_proc_called = false
update_first_migration_date(11.minutes.ago)
Migration::ColumnDropper.drop(
table: 'topics',
after_migration: migration_name,
columns: ['junk'],
delay: 30.minutes,
on_drop: ->() { dropped_proc_called = true }
)
expect(has_column?('topics', 'junk')).to eq(true)
expect(dropped_proc_called).to eq(false)
update_first_migration_date(9.minutes.ago)
Migration::ColumnDropper.drop(
table: 'topics',
after_migration: migration_name,
columns: ['junk'],
delay: 30.minutes,
on_drop: ->() { dropped_proc_called = true }
)
expect(has_column?('topics', 'junk')).to eq(false)
expect(dropped_proc_called).to eq(true)
end
end
describe '.mark_readonly' do
let(:table_name) { "table_with_readonly_column" }
before do
DB.exec <<~SQL
CREATE TABLE #{table_name} (topic_id INTEGER, email TEXT);
INSERT INTO #{table_name} (topic_id, email)
VALUES (1, 'something@email.com');
SQL
Migration::ColumnDropper.mark_readonly(table_name, 'email')
end
after do
ActiveRecord::Base.connection.reset!
DB.exec <<~SQL
DROP TABLE IF EXISTS #{table_name};
DROP TRIGGER IF EXISTS #{table_name}_email_readonly ON #{table_name};
SQL
end
it 'should be droppable' do
name = DB.query_single("SELECT name FROM schema_migration_details LIMIT 1").first
dropped_proc_called = false
Migration::ColumnDropper.drop(
table: table_name,
after_migration: name,
columns: ['email'],
delay: 0.minutes,
on_drop: ->() { dropped_proc_called = true }
)
expect(dropped_proc_called).to eq(true)
end
it 'should prevent updates to the readonly column' do
expect do
DB.exec <<~SQL
UPDATE #{table_name}
SET email = 'testing@email.com'
WHERE topic_id = 1;
SQL
end.to raise_error(
PG::RaiseException,
/Discourse: email in #{table_name} is readonly/
)
end
it 'should allow updates to the other columns' do
DB.exec <<~SQL
UPDATE #{table_name}
SET topic_id = 2
WHERE topic_id = 1
SQL
expect(
ActiveRecord::Base.exec_sql("SELECT * FROM #{table_name};").values
).to include([2, "something@email.com"])
end
it 'should prevent insertions to the readonly column' do
expect do
ActiveRecord::Base.connection.raw_connection.exec <<~SQL
INSERT INTO #{table_name} (topic_id, email)
VALUES (2, 'something@email.com');
SQL
end.to raise_error(
PG::RaiseException,
/Discourse: email in table_with_readonly_column is readonly/
)
end
it 'should allow insertions to the other columns' do
DB.exec <<~SQL
INSERT INTO #{table_name} (topic_id)
VALUES (2);
SQL
expect(
DB.query_single("SELECT topic_id FROM #{table_name} WHERE topic_id = 2")
).to eq([2])
end
end
end