Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare support for other database adapters #11

Merged
merged 3 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ PATH
specs:
veksel (0.2.2)
activerecord
pg
rails (>= 7.1.0, < 8)

GEM
Expand Down
2 changes: 0 additions & 2 deletions bin/veksel
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
require 'veksel/cli'

case ARGV[0]
when 'suffix'
Veksel::CLI.suffix
when 'fork'
Veksel::CLI.fork
end
12 changes: 10 additions & 2 deletions lib/tasks/veksel_tasks.rake
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ namespace :veksel do
require 'veksel/commands/list'

db = ActiveRecord::Base.configurations.find_db_config('development')
Veksel::Commands::List.new(db).perform
begin
Veksel::Commands::List.new(db).perform
rescue Veksel::AdapterNotSupported => e
$stderr.puts e.message
end
end

desc "Delete forked databases"
task clean: 'db:load_config' do
require 'veksel/commands/clean'

db = ActiveRecord::Base.configurations.find_db_config('development')
Veksel::Commands::Clean.new(db).perform
begin
Veksel::Commands::Clean.new(db).perform
rescue Veksel::AdapterNotSupported => e
$stderr.puts "#{e.message} - clean skipped"
end
end
end
17 changes: 13 additions & 4 deletions lib/veksel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@
require "veksel/suffix"

module Veksel
class AdapterNotSupported < StandardError; end

class << self
def adapter_for(config, exception: true)
case config[:adapter]
when 'postgresql'
require_relative './veksel/pg_cluster'
Veksel::PgCluster.new(config)
else
return unless exception
raise AdapterNotSupported, "Veksel does not yet support #{config[:adapter]}"
end
end

def current_branch
`git rev-parse --abbrev-ref HEAD`.strip
end
Expand All @@ -25,9 +38,5 @@ def skip_fork?
def suffix
Suffix.new(current_branch).to_s
end

def prefix(dbname)
dbname.sub(%r[#{Veksel.suffix}$], '_')
end
end
end
21 changes: 10 additions & 11 deletions lib/veksel/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@

module Veksel
module CLI
class << self
def suffix
print Veksel.suffix
end
DbConfig = Struct.new('DbConfig', :configuration_hash)

class << self
def fork
return if Veksel.skip_fork?
t1 = Time.now.to_f

require 'veksel/commands/fork'
require 'psych'
require 'ostruct'
require 'fileutils'

config = read_config('config/database.yml')[:development]
Expand All @@ -23,12 +20,14 @@ def fork
end

target_database = config[:database] + Veksel.suffix
db = OpenStruct.new(configuration_hash: config, database: target_database)
Veksel::Commands::Fork.new(db).perform

duration = ((Time.now.to_f - t1) * 1000).to_i
FileUtils.touch('tmp/restart.txt')
puts "Forked database in #{duration.to_i}ms"
db = DbConfig.new(config)
if Veksel::Commands::Fork.new(db).perform
duration = ((Time.now.to_f - t1) * 1000).to_i
FileUtils.touch('tmp/restart.txt')
puts "Forked database in #{duration.to_i}ms"
end
rescue AdapterNotSupported => e
$stderr.puts "#{e.message} - fork skipped"
end

def read_config(path)
Expand Down
10 changes: 4 additions & 6 deletions lib/veksel/commands/clean.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
require_relative '../pg_cluster'

module Veksel
module Commands
class Clean
def initialize(db, dry_run: false)
@pg_cluster = PgCluster.new(db.configuration_hash)
@adapter = Veksel.adapter_for(db.configuration_hash)
@dry_run = dry_run
end

def perform
all_databases = @pg_cluster.forked_databases
all_databases = @adapter.forked_databases
active_branches = Veksel.active_branches

stale_databases = all_databases.filter do |database|
active_branches.none? { |branch| database.end_with?("_#{branch}") }
active_branches.none? { |branch| database.branch == branch }
end
stale_databases.each do |database|
@pg_cluster.drop_database(database, dry_run: @dry_run)
@adapter.drop_database(database.name, dry_run: @dry_run)
end
end
end
Expand Down
19 changes: 10 additions & 9 deletions lib/veksel/commands/fork.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
require_relative '../pg_cluster'

module Veksel
module Commands
class Fork
attr_reader :pg_cluster, :source_db, :target_db
attr_reader :adapter, :source_db, :target_db

def initialize(db)
@pg_cluster = PgCluster.new(db.configuration_hash)
@source_db = db.database.sub(%r[#{Veksel.suffix}$], '')
@target_db = db.database
@adapter = Veksel.adapter_for(db.configuration_hash)
@source_db = adapter.main_database
@target_db = adapter.db_name_for_suffix(Veksel.suffix)
raise "Source and target databases cannot be the same" if source_db == target_db
end

def perform
return if pg_cluster.target_populated?(target_db)
return false unless adapter
return false if adapter.target_populated?(target_db)

adapter.kill_connection(source_db)
adapter.create_database(target_db, template: source_db)

pg_cluster.kill_connection(source_db)
pg_cluster.create_database(target_db, template: source_db)
true
end
end
end
Expand Down
8 changes: 3 additions & 5 deletions lib/veksel/commands/list.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
require_relative '../pg_cluster'

module Veksel
module Commands
class List
attr_reader :adapter

def initialize(db)
@adapter = PgCluster.new(db.configuration_hash)
@adapter = Veksel.adapter_for(db.configuration_hash)
end

def perform
Expand All @@ -20,8 +18,8 @@ def perform

hash = {}
databases.each do |database|
branch = database.sub(adapter.forked_database_prefix, '')
hash[branch] = database
branch = database.branch
hash[branch] = database.name
end

longest_branch_name = hash.keys.max_by(&:length).length
Expand Down
15 changes: 12 additions & 3 deletions lib/veksel/pg_cluster.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Veksel
class PgCluster
Database = Struct.new(:name, :branch)

attr_reader :configuration_hash

def initialize(configuration_hash)
Expand All @@ -11,11 +13,14 @@ def main_database
end

def forked_databases
list_databases(prefix: forked_database_prefix)
list_databases(prefix: forked_database_prefix).map do |name|
Database.new(name, name.sub(forked_database_prefix, ''))
end
end

def forked_database_prefix
"#{main_database}_"
def db_name_for_suffix(suffix)
return main_database if suffix.empty?
"#{forked_database_prefix}#{suffix}"
end

def target_populated?(dbname)
Expand Down Expand Up @@ -54,6 +59,10 @@ def drop_database(dbname, dry_run: false)

private

def forked_database_prefix
"#{main_database}_"
end

def pg_connection_args(dbname)
"-d #{dbname} --no-password"
end
Expand Down
17 changes: 9 additions & 8 deletions lib/veksel/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ class Railtie < ::Rails::Railtie
ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
next if url.present?
next unless env_name == 'development' || env_name == 'test'
veksel_adapter = Veksel.adapter_for(config, exception: false)
next unless veksel_adapter

if PgCluster.new(config).target_populated?("#{config[:database]}#{Veksel.suffix}")
ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, name, config.merge({
database: "#{config[:database]}#{Veksel.suffix}",
veksel_main_database: config[:database],
}))
else
ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, name, config)
end
database_name = veksel_adapter.db_name_for_suffix(Veksel.suffix)
next unless veksel_adapter.target_populated?(database_name)

ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, name, config.merge({
database: database_name,
veksel_main_database: config[:database],
}))
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/veksel/suffix.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def to_s
when *PROTECTED_BRANCHES
""
else
"_#{@branch_name}"
@branch_name
end
end
end
Expand Down
7 changes: 7 additions & 0 deletions test/dummy/config/database.sqlite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
development:
adapter: sqlite3
database: db/development.sqlite3

test:
adapter: sqlite3
database: db/test.sqlite3
10 changes: 10 additions & 0 deletions test/pg_cluster_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require "test_helper"


class VekselPgClusterTest < ActiveSupport::TestCase
test "db_name_for_suffix" do
pg_cluster = Veksel::PgCluster.new({ database: 'veksel_dummy_development' })
assert_equal 'veksel_dummy_development_somebranch', pg_cluster.db_name_for_suffix('somebranch')
assert_equal 'veksel_dummy_development', pg_cluster.db_name_for_suffix('')
end
end
36 changes: 36 additions & 0 deletions test/veksel_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ class VekselTest < ActiveSupport::TestCase
assert Veksel::VERSION
end

test "use default database in main branch" do
Dir.chdir('test/dummy') do
current_db = `bin/rails runner "print ApplicationRecord.connection.execute('SELECT current_database();')[0]['current_database']"`.chomp
assert_equal 'veksel_dummy_development', current_db
end
end

test "use default database in master branch" do
Dir.chdir('test/dummy') do
git_checkout('master') do
current_db = `bin/rails runner "print ApplicationRecord.connection.execute('SELECT current_database();')[0]['current_database']"`.chomp
assert_equal 'veksel_dummy_development', current_db
end
end
end

class IntegrationTests < ActiveSupport::TestCase
include TestHelpers

Expand Down Expand Up @@ -65,6 +81,26 @@ def run_fork_test
end
end

test "veksel fork should warn on unsupported database adapters" do
swap_db_config('database.sqlite.yml') do
Dir.chdir('test/dummy') do
begin
system!('bin/rails db:setup')
system!('bin/rails veksel:clean')

git_checkout('somebranch') do
Tempfile.open do |buffer|
system!('bundle exec veksel fork', exception: false, err: buffer.path.to_s)
buffer.rewind
output = buffer.read
assert_match /Veksel does not yet support sqlite3 - fork skipped/, output
end
end
end
end
end
end

test "performance" do
def measure_in_ms(&blk)
t0 = (Time.now.to_f * 1000).to_i
Expand Down
1 change: 0 additions & 1 deletion veksel.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@ Gem::Specification.new do |spec|

spec.add_dependency "rails", ">= 7.1.0", "< 8"
spec.add_dependency "activerecord"
spec.add_dependency "pg"
end