veksel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 91758ab594f11138bbfb291449ca7a89e5de19e6cee85e024a2bb03b7c3056d1
4
+ data.tar.gz: 28935ca101031e85fa0a86e5163408b5fc5cf2c25a8cea9b3c351901a04e96cd
5
+ SHA512:
6
+ metadata.gz: 22592284f0ad8be05605deaed84019da1a8b3c136cd8a1492680a6a83424ad3307193c74b44c5c581fadf9f63efe66e848c0b1f2b7933748c2215f937bf334ff
7
+ data.tar.gz: 1ed0d8d73e466a24e2cac00a4d334cfb834a3df7c03a621c34015dc05172c0abb8088c70984614d9dd0e4cf7e77fa2f37c5e72756c4ed0336ae8e521c61b2dc5
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Theodor Tonum
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Veksel: Database branching for Rails
2
+
3
+ Veksel keeps seperate databases for every branch in your development environment. This makes it easy to experiment with schema changes and data with less risk and avoid conflicting changes to `schema.rb` when branches have different sets of migrations. The inspiration for the gem came from [branch support in Neon](https://neon.tech/docs/manage/branches).
4
+
5
+ Postgresql is currently the only supported database driver.
6
+
7
+ ## Usage
8
+
9
+ Change the following line in `config/database.yml`
10
+
11
+ ```yaml
12
+ development:
13
+ database: your_app_development<%= `bundle exec veksel suffix` %>
14
+ ```
15
+
16
+ Checkout a new branch and run `bin/rails veksel:fork`. A new database with a suffix matching your branch name will be created.
17
+
18
+ ### Veksel tasks
19
+
20
+ ```
21
+ veksel:clean Delete forked databases
22
+ veksel:fork Fork the database from the main branch
23
+ veksel:list List forked databases
24
+ ```
25
+
26
+ ## Git hook
27
+
28
+ Add the following to `.git/hooks/post-checkout` to automatically fork your database when checking out a branch:
29
+
30
+ ```
31
+ #!/bin/sh
32
+ bin/rails veksel:fork
33
+ ```
34
+
35
+ ## Installation
36
+
37
+ Add this line to your application's Gemfile:
38
+
39
+ ```ruby
40
+ gem "veksel", group: :development
41
+ ```
42
+
43
+ And then execute:
44
+
45
+ ```bash
46
+ $ bundle
47
+ ```
48
+
49
+ Or install it yourself as:
50
+
51
+ ```bash
52
+ $ gem install veksel
53
+ ```
54
+
55
+ ## Roadmap
56
+
57
+ - Promote a forked database to main
58
+ - Explicit/optional branching
59
+ - Other database drivers
60
+
61
+ ## License
62
+
63
+ Veksel is licensed under MIT.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
data/bin/veksel ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require 'veksel'
3
+
4
+ case ARGV[0]
5
+ when 'suffix'
6
+ print Veksel.suffix
7
+ end
@@ -0,0 +1,69 @@
1
+ namespace :veksel do
2
+ task :precreate do
3
+ ActiveSupport::Notifications.subscribe "veksel.fork" do |*args|
4
+ event = ActiveSupport::Notifications::Event.new(*args)
5
+ puts "Forked database in #{event.duration.to_i}ms"
6
+ File.open("log/veksel.log", "a") do |f|
7
+ f.puts("Forked database in #{event.duration.to_i}ms")
8
+ f.puts(" Source: #{event.payload[:source]}")
9
+ f.puts(" Target: #{event.payload[:target]}")
10
+ end
11
+ end
12
+ end
13
+
14
+ desc "Fork the database from the main branch"
15
+ task fork: ['db:create', 'veksel:precreate'] do
16
+ next if Veksel.skip_fork?
17
+ require 'veksel/commands/fork'
18
+
19
+ db = ActiveRecord::Base.configurations.find_db_config('development')
20
+ Veksel::Commands::Fork.new(db).perform
21
+ end
22
+
23
+ desc "List forked databases"
24
+ task list: 'db:load_config' do
25
+ require 'veksel/commands/clean'
26
+
27
+ db = ActiveRecord::Base.configurations.find_db_config('development')
28
+ command = Veksel::Commands::Clean.new(db)
29
+ databases = command.all_databases
30
+ active_branches = command.active_branches
31
+
32
+ if databases.empty?
33
+ puts "No databases created by Veksel"
34
+ next
35
+ end
36
+
37
+ hash = {}
38
+ databases.each do |database|
39
+ branch = database.sub(command.prefix, '')
40
+ hash[branch] = database
41
+ end
42
+
43
+ longest_branch_name = hash.keys.max_by(&:length).length
44
+ longest_database_name = hash.values.max_by(&:length).length
45
+ puts "Databases created by Veksel:"
46
+ puts ""
47
+ puts "#{'Branch'.ljust(longest_branch_name)} #{'Database'.ljust(longest_database_name)} Active"
48
+ inactive_count = 0
49
+ hash.each do |branch, database|
50
+ # Print a formatted string padded to fit the longest branch name
51
+ active = active_branches.include?(branch) ? 'Yes' : 'No'
52
+ inactive_count += 1 if active == 'No'
53
+ puts "#{branch.ljust(longest_branch_name)} #{database.ljust(longest_database_name)} #{active}"
54
+ end
55
+
56
+ if inactive_count > 0
57
+ puts ""
58
+ puts "Clean inactive databases with bin/rails veksel:clean"
59
+ end
60
+ end
61
+
62
+ desc "Delete forked databases"
63
+ task clean: 'db:load_config' do
64
+ require 'veksel/commands/clean'
65
+
66
+ db = ActiveRecord::Base.configurations.find_db_config('development')
67
+ Veksel::Commands::Clean.new(db).perform
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ require_relative '../pg_cluster'
2
+
3
+ module Veksel
4
+ module Commands
5
+ class Clean
6
+ attr_reader :prefix
7
+
8
+ def initialize(db, dry_run: false)
9
+ @pg_cluster = PgCluster.new(db.configuration_hash)
10
+ @prefix = Veksel.prefix(db.configuration_hash[:database])
11
+ @dry_run = dry_run
12
+ end
13
+
14
+ def perform
15
+ all_databases = @pg_cluster.list_databases(prefix: @prefix)
16
+ stale_databases = all_databases.filter do |database|
17
+ active_branches.none? { |branch| database.end_with?("_#{branch}") }
18
+ end
19
+ stale_databases.each do |database|
20
+ @pg_cluster.drop_database(database, dry_run: @dry_run)
21
+ end
22
+ end
23
+
24
+ def active_branches
25
+ `git for-each-ref 'refs/heads/' --format '%(refname)'`.split("\n").map { |ref| ref.sub('refs/heads/', '') }
26
+ end
27
+
28
+ def all_databases
29
+ @pg_cluster.list_databases(prefix: @prefix)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../pg_cluster'
2
+
3
+ module Veksel
4
+ module Commands
5
+ class Fork
6
+ attr_reader :pg_cluster, :source_db, :target_db
7
+
8
+ def initialize(db)
9
+ @pg_cluster = PgCluster.new(db.configuration_hash)
10
+ @source_db = db.database.sub(%r[#{Veksel.suffix}$], '')
11
+ @target_db = db.database
12
+ raise "Source and target databases cannot be the same" if source_db == target_db
13
+ end
14
+
15
+ def perform
16
+ return if pg_cluster.target_populated?(target_db)
17
+
18
+ ActiveSupport::Notifications.instrument "veksel.fork", source: source_db, target: target_db do
19
+ pg_cluster.transfer(from: source_db, to: target_db)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ module Veksel
2
+ class PgCluster
3
+ attr_reader :configuration_hash
4
+
5
+ def initialize(configuration_hash)
6
+ @configuration_hash = configuration_hash
7
+ end
8
+
9
+ def target_populated?(dbname)
10
+ IO.pipe do |r, w|
11
+ spawn(pg_env, %[psql -t #{pg_connection_args(dbname)} -c "SELECT 'ok' FROM ar_internal_metadata LIMIT 1;"], out: w, err: '/dev/null')
12
+ pid_grep = spawn(pg_env, %[grep -qw ok], in: r, err: '/dev/null')
13
+ w.close
14
+ Process::Status.wait(pid_grep).success?
15
+ end
16
+ end
17
+
18
+ def transfer(from:, to:)
19
+ r, w = IO.pipe(autoclose: true)
20
+ spawn(pg_env, "pg_dump #{pg_connection_args(from)} --format=c", out: w)
21
+ pid_psql = spawn(pg_env, "pg_restore --single-transaction --exit-on-error #{pg_connection_args(to)}", in: r)
22
+ unless Process::Status.wait(pid_psql).success?
23
+ # TODO: Write error log to a tempfile inside the tmp directory of the current directory
24
+ raise "pg_restore failed with status #{status.exitstatus}"
25
+ end
26
+ end
27
+
28
+ def list_databases(prefix:)
29
+ IO.pipe(autoclose: true) do |r, w|
30
+ sql = %[SELECT datname FROM pg_database WHERE datname LIKE '#{prefix}%'];
31
+ psql = spawn(pg_env, %[psql -t #{pg_connection_args('postgres')} -c "#{sql}"], out: w, err: '/dev/null')
32
+ w.close
33
+ Process::Status.wait(psql)
34
+ r.read.split("\n").map(&:strip)
35
+ end
36
+ end
37
+
38
+ def drop_database(dbname, dry_run: false)
39
+ if dry_run
40
+ puts "[Veksel] Would drop database #{dbname}"
41
+ end
42
+ spawn(pg_env, %[dropdb --no-password #{dbname}])
43
+ end
44
+
45
+ private
46
+
47
+ def pg_connection_args(dbname)
48
+ "-d #{dbname} --no-password"
49
+ end
50
+
51
+ def pg_env
52
+ {
53
+ 'PGHOST' => configuration_hash[:host],
54
+ 'PGPORT' => configuration_hash[:port]&.to_s,
55
+ 'PGUSER' => configuration_hash[:username],
56
+ 'PGPASSWORD' => configuration_hash[:password],
57
+ }.compact
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,7 @@
1
+ module Veksel
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/veksel_tasks.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module Veksel
2
+ PROTECTED_BRANCHES = %w[master main HEAD].freeze
3
+
4
+ class Suffix
5
+ def initialize(branch_name)
6
+ @branch_name = branch_name
7
+ end
8
+
9
+ def to_s
10
+ case @branch_name
11
+ when *PROTECTED_BRANCHES
12
+ ""
13
+ else
14
+ "_#{@branch_name}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Veksel
2
+ VERSION = "0.1.0"
3
+ end
data/lib/veksel.rb ADDED
@@ -0,0 +1,29 @@
1
+ if defined?(Rails::VERSION)
2
+ if Rails::VERSION::MAJOR < 6
3
+ raise "Veksel requires Rails 6 or later"
4
+ end
5
+ end
6
+
7
+ require "veksel/version"
8
+ require "veksel/railtie" if defined?(Rails::Railtie)
9
+ require "veksel/suffix"
10
+
11
+ module Veksel
12
+ class << self
13
+ def current_branch
14
+ `git rev-parse --abbrev-ref HEAD`.strip
15
+ end
16
+
17
+ def skip_fork?
18
+ suffix.blank?
19
+ end
20
+
21
+ def suffix
22
+ Suffix.new(current_branch).to_s
23
+ end
24
+
25
+ def prefix(dbname)
26
+ dbname.sub(%r[#{Veksel.suffix}$], '_')
27
+ end
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: veksel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Theodor Tonum
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '6'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pg
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: appraisal
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ description: Seperate databases for every branch in your development environment
76
+ email:
77
+ - theodor@tonum.no
78
+ executables:
79
+ - veksel
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - LICENSE.md
84
+ - README.md
85
+ - Rakefile
86
+ - bin/veksel
87
+ - lib/tasks/veksel_tasks.rake
88
+ - lib/veksel.rb
89
+ - lib/veksel/commands/clean.rb
90
+ - lib/veksel/commands/fork.rb
91
+ - lib/veksel/pg_cluster.rb
92
+ - lib/veksel/railtie.rb
93
+ - lib/veksel/suffix.rb
94
+ - lib/veksel/version.rb
95
+ homepage: https://github.com/theodorton/veksel
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ allowed_push_host: https://rubygems.org
100
+ homepage_uri: https://github.com/theodorton/veksel
101
+ source_code_uri: https://github.com/theodorton/veksel
102
+ changelog_uri: https://github.com/theodorton/veksel/blob/main/CHANGELOG.md
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.4.19
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Veksel keeps seperate databases for every branch in your development environment.
122
+ This makes it easy to experiment with schema changes and data with less risk and
123
+ schema.rb headache when switching branches that has different sets of migrations.
124
+ The inspiration for the gem came from neons branch support.
125
+ test_files: []