veksel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []