discourse/migrations/spec/lib/database/connection_spec.rb
Gerhard Schlager 7c3a29c9d6
DEV: Add converter framework for migrations-tooling (#28540)
* Updates GitHub Actions
* Switches from `bundler/inline` to an optional group in the `Gemfile` because the previous solution didn't work well with rspec
* Adds the converter framework and tests
* Allows loading private converters (see README)
* Switches from multiple CLI tools to a single CLI
* Makes DB connections reusable and adds a new abstraction for the `IntermediateDB`
* `IntermediateDB` acts as an interface for IPC calls when a converter steps runs in parallel (forks). Only the main process writes to the DB.
* Includes a simple example implementation of a converter for now.
2024-09-09 17:14:39 +02:00

177 lines
5.3 KiB
Ruby

# frozen_string_literal: true
RSpec.describe ::Migrations::Database::Connection do
def create_connection(**params)
Dir.mktmpdir do |storage_path|
db_path = File.join(storage_path, "test.db")
connection = described_class.new(path: db_path, **params)
return connection if !block_given?
begin
yield connection
ensure
connection.close if connection
end
end
end
describe "class" do
subject(:connection) { create_connection }
after { connection.close }
it_behaves_like "a database connection"
end
describe ".open_database" do
it "creates a database at the given path " do
Dir.mktmpdir do |storage_path|
db_path = File.join(storage_path, "test.db")
db = described_class.open_database(path: db_path)
expect(File.exist?(db_path)).to be true
expect(db.pragma("journal_mode")).to eq("wal")
expect(db.pragma("locking_mode")).to eq("normal")
ensure
db.close if db
end
end
end
describe "#close" do
it "closes the underlying database" do
create_connection do |connection|
db = connection.db
connection.close
expect(db).to be_closed
end
end
it "closes cached prepared statements" do
cache_class = ::Migrations::Database::PreparedStatementCache
cache_double = instance_spy(cache_class)
allow(cache_class).to receive(:new).and_return(cache_double)
create_connection do |connection|
expect(cache_double).not_to have_received(:clear)
connection.close
expect(cache_double).to have_received(:clear).once
end
end
it "commits an active transaction" do
create_connection do |connection|
db = described_class.open_database(path: connection.path)
db.execute("CREATE TABLE foo (id INTEGER)")
connection.insert("INSERT INTO foo (id) VALUES (?)", [1])
connection.insert("INSERT INTO foo (id) VALUES (?)", [2])
expect(db.query_single_splat("SELECT COUNT(*) FROM foo")).to eq(0)
connection.close
expect(db.query_single_splat("SELECT COUNT(*) FROM foo")).to eq(2)
db.close
end
end
end
describe "#closed?" do
it "correctly reports if connection is closed" do
create_connection do |connection|
expect(connection.closed?).to be false
connection.close
expect(connection.closed?).to be true
end
end
end
describe "#insert" do
it "commits inserted rows when reaching `batch_size`" do
transaction_batch_size = 3
create_connection(transaction_batch_size:) do |connection|
db = described_class.open_database(path: connection.path)
db.execute("CREATE TABLE foo (id INTEGER)")
1.upto(10) do |index|
connection.insert("INSERT INTO foo (id) VALUES (?)", [index])
expected_count = index / transaction_batch_size * transaction_batch_size
expect(db.query_single_splat("SELECT COUNT(*) FROM foo")).to eq(expected_count)
end
db.close
end
end
it "works with one and more parameters" do
transaction_batch_size = 1
create_connection(transaction_batch_size:) do |connection|
db = described_class.open_database(path: connection.path)
db.execute("CREATE TABLE foo (id INTEGER)")
db.execute("CREATE TABLE bar (id INTEGER, name TEXT)")
connection.insert("INSERT INTO foo (id) VALUES (?)", [1])
connection.insert("INSERT INTO bar (id, name) VALUES (?, ?)", [1, "Alice"])
expect(db.query_splat("SELECT id FROM foo")).to contain_exactly(1)
expect(db.query("SELECT id, name FROM bar")).to contain_exactly({ id: 1, name: "Alice" })
db.close
end
end
end
context "when `::Migrations::ForkManager.fork` is used" do
it "temporarily closes the connection while a process fork is created" do
create_connection do |connection|
expect(connection.closed?).to be false
connection.db.execute("CREATE TABLE foo (id INTEGER)")
connection.insert("INSERT INTO foo (id) VALUES (?)", [1])
expect(connection.db.query_splat("SELECT id FROM foo")).to contain_exactly(1)
db_before_fork = connection.db
::Migrations::ForkManager.fork do
expect(connection.closed?).to be true
expect(connection.db).to be_nil
end
expect(connection.closed?).to be false
expect(connection.db).to_not eq(db_before_fork)
connection.insert("INSERT INTO foo (id) VALUES (?)", [2])
expect(connection.db.query_splat("SELECT id FROM foo")).to contain_exactly(1, 2)
end
end
it "works with multiple forks" do
create_connection do |connection|
expect(connection.closed?).to be false
::Migrations::ForkManager.fork { expect(connection.closed?).to be true }
expect(connection.closed?).to be false
::Migrations::ForkManager.fork { expect(connection.closed?).to be true }
expect(connection.closed?).to be false
end
end
it "cleans up fork hooks when connection gets closed" do
expect(::Migrations::ForkManager.size).to eq(0)
create_connection do |connection|
expect(::Migrations::ForkManager.size).to eq(2)
connection.close
expect(::Migrations::ForkManager.size).to eq(0)
end
end
end
end