songbird 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: f4b3d882caf1f2537fe3904c8e24a56fb58b0ac3edb46907f7c183e5afdaf3a9
4
+ data.tar.gz: dec81593f25440661a062486f6e33b4063642a52d702477b6fadd7547c1b5c15
5
+ SHA512:
6
+ metadata.gz: ea0a7bcf2c5ac38d28f2f42d0150532cd5eff59aa045e8f8320830158df7d4013c28727659dd89d5a0c2f22e0565be18dd8441c8d077612aba641af8fe3d8dc3
7
+ data.tar.gz: 9468d3b43d4c1111bbbae0040787769d9fef9d86dc9d06a7a9fed2045d09bf4919b2625b2051230e8ad1fa297bf14f9df343a197ac585f67266aaccacccd7e9a
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Elcio Nakashima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # 🐦 Songbird
2
+
3
+ Extract SQL statements from Rails migrations for manual review and execution. Perfect for debugging, review, or manual execution of schema changes.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'songbird'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it directly:
20
+
21
+ ```bash
22
+ $ gem install songbird
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Songbird provides Rake tasks that integrate seamlessly with your Rails application to generate SQL for migrations.
28
+
29
+ ### Basic Usage
30
+
31
+ Generate SQL for a specific migration:
32
+ ```bash
33
+ rake db:migrate:sql[20241201000000]
34
+ ```
35
+
36
+ Running the command without a version will show usage instructions and list your three most recent migrations for easy reference.
37
+
38
+ ### Shortcut Commands
39
+
40
+ Songbird also provides a convenient shortcut command:
41
+
42
+ ```bash
43
+ rake songbird:sql[20241201000000] # Generate SQL for specific migration
44
+ ```
45
+
46
+ ### Output Format
47
+
48
+ The generated SQL includes helpful comments and proper formatting:
49
+
50
+ ```sql
51
+ -- Generated SQL for pending Rails migrations
52
+ -- Generated at: 2024-12-01 10:30:00 UTC
53
+
54
+ -- Migration: 20241201000001 - Create users
55
+ -- File: 20241201000001_create_users.rb
56
+ CREATE TABLE "users" (id SERIAL PRIMARY KEY);
57
+ ALTER TABLE "users" ADD COLUMN "name" VARCHAR NOT NULL;
58
+
59
+ -- Update schema_migrations table:
60
+ INSERT INTO schema_migrations (version) VALUES ('20241201000001');
61
+
62
+ -- Migration: 20241201000002 - Add email to users
63
+ -- File: 20241201000002_add_email_to_users.rb
64
+ ALTER TABLE "users" ADD COLUMN "email" VARCHAR;
65
+
66
+ -- Update schema_migrations table:
67
+ INSERT INTO schema_migrations (version) VALUES ('20241201000002');
68
+ ```
69
+
70
+ ## Features
71
+
72
+ - ✅ **Database-Agnostic** - Works with any ActiveRecord-supported database
73
+ - ✅ **Rails Integration** - Works seamlessly with Rails migration system
74
+ - ✅ **Schema Metadata** - Automatically generates `schema_migrations` INSERT statements
75
+ - ✅ **Clean Output Format** - Properly commented SQL ready for database execution
76
+ - ✅ **Specific Migration Support** - Generate SQL for individual migrations by version
77
+ - ✅ **Complete Migration Support** - Supports all Rails migration operations by intercepting at the SQL level
78
+
79
+ ## Use Cases
80
+
81
+ ### Migration Timeouts
82
+ When Rails migrations timeout in production due to long-running operations, they can leave migrations in an incomplete state that requires manual intervention. With Songbird:
83
+
84
+ 1. Run `rake db:migrate:sql[specific_migration_version]` for the failed migration
85
+ 2. Execute the generated SQL manually in your database
86
+ 3. Resume normal Rails migrations
87
+
88
+ ### Database Review Process
89
+ For organizations requiring DBA approval of schema changes:
90
+
91
+ 1. Generate SQL using Songbird
92
+ 2. Submit SQL for review
93
+ 3. Execute approved SQL manually
94
+ 4. Update `schema_migrations` table
95
+
96
+ ### Complex Migration Debugging
97
+ Debug migration issues by reviewing the generated SQL before execution:
98
+
99
+ 1. Generate SQL to understand what operations will be performed
100
+ 2. Identify potential issues (missing indexes, constraint violations, etc.)
101
+ 3. Modify migrations as needed
102
+
103
+
104
+ ## Development
105
+
106
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
107
+
108
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
109
+
110
+ ## License
111
+
112
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
113
+
114
+ ## Contributing
115
+
116
+ Bug reports and pull requests are welcome on GitHub at https://github.com/elciok/songbird.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Songbird
6
+ class MigrationDiscoverer
7
+ attr_reader :migrations_path
8
+
9
+ def initialize(migrations_path = nil)
10
+ @migrations_path = migrations_path || default_migrations_path
11
+ end
12
+
13
+ # Get all available migrations
14
+ def load_all_migrations
15
+ load_migration_files
16
+ end
17
+
18
+ # Load a specific migration class by version
19
+ def load_migration_class(version)
20
+ migration_info = find_migration_by_version_private(version)
21
+ return nil unless migration_info
22
+
23
+ # Load the migration file
24
+ load migration_info[:filepath]
25
+
26
+ # Get the migration class
27
+ migration_info[:class_name].constantize
28
+ end
29
+
30
+ # Find migration info by version
31
+ def find_migration_by_version(version)
32
+ find_migration_by_version_private(version)
33
+ end
34
+
35
+ # Check if migrations directory exists
36
+ def migrations_exist?
37
+ Dir.exist?(@migrations_path)
38
+ end
39
+
40
+ # Get the path to migrations directory
41
+ def migrations_path
42
+ @migrations_path
43
+ end
44
+
45
+ private
46
+
47
+ def default_migrations_path
48
+ if defined?(Rails)
49
+ # Use Rails' configured migrations paths - this is more accurate than hardcoding
50
+ # Rails can have multiple migration paths and this gets the primary one
51
+ ActiveRecord::Migrator.migrations_paths.first || Rails.root.join('db', 'migrate').to_s
52
+ else
53
+ File.join(Dir.pwd, 'db', 'migrate')
54
+ end
55
+ end
56
+
57
+ def load_migration_files
58
+ return [] unless migrations_exist?
59
+
60
+ migration_files = Dir.glob(File.join(@migrations_path, '*.rb')).sort
61
+
62
+ migration_files.map do |filepath|
63
+ filename = File.basename(filepath, '.rb')
64
+ version, name = extract_version_and_name(filename)
65
+
66
+ next unless version
67
+
68
+ {
69
+ version: version,
70
+ name: name,
71
+ filename: filename,
72
+ filepath: filepath,
73
+ class_name: name.camelize
74
+ }
75
+ end.compact
76
+ end
77
+
78
+ def extract_version_and_name(filename)
79
+ # Migration files follow pattern: YYYYMMDDHHMMSS_migration_name.rb
80
+ if match = filename.match(/\A(\d{14})_(.+)\z/)
81
+ version = match[1]
82
+ name = match[2]
83
+ [version, name]
84
+ else
85
+ [nil, nil]
86
+ end
87
+ end
88
+
89
+ def find_migration_by_version_private(version)
90
+ all_migrations = load_migration_files
91
+ all_migrations.find { |m| m[:version] == version.to_s }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Songbird
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load File.expand_path('../tasks/songbird.rake', __dir__)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+ require 'ostruct'
5
+ require 'logger'
6
+ require_relative 'migration_discoverer'
7
+
8
+ module Songbird
9
+ class SQLGenerator
10
+ attr_reader :discoverer
11
+
12
+ def initialize(migrations_path: nil)
13
+ @discoverer = MigrationDiscoverer.new(migrations_path)
14
+ end
15
+
16
+ # Process migration request - generate SQL for a specific migration or show usage
17
+ def process(version: nil, output_format: :raw)
18
+ unless @discoverer.migrations_exist?
19
+ raise "Migrations directory not found: #{@discoverer.migrations_path}"
20
+ end
21
+
22
+ if version
23
+ # Generate SQL for specific migration
24
+ migration_info = @discoverer.find_migration_by_version(version)
25
+ if migration_info.nil?
26
+ return "-- Migration #{version} not found"
27
+ end
28
+
29
+ migration_sql = generate_migration_sql(migration_info)
30
+ schema_sql = generate_schema_migration_sql(migration_info[:version])
31
+ sql_statement = format_migration_block(migration_info, migration_sql, schema_sql, output_format)
32
+
33
+ case output_format
34
+ when :commented
35
+ format_with_comments([sql_statement])
36
+ else
37
+ sql_statement
38
+ end
39
+ else
40
+ # Show usage with recent migrations when no version specified
41
+ recent_migrations = @discoverer.load_all_migrations.last(3)
42
+
43
+ usage_text = <<~USAGE
44
+ -- Songbird: Generate SQL for Rails migrations
45
+ --
46
+ -- Usage: rake db:migrate:sql[VERSION]
47
+ -- Example: rake db:migrate:sql[20241201000001]
48
+ --
49
+ -- This will generate the SQL commands for the specified migration
50
+ -- that you can execute manually in your database.
51
+ USAGE
52
+
53
+ if recent_migrations.any?
54
+ usage_text += "\n-- Recent migrations:\n"
55
+ recent_migrations.reverse.each do |migration|
56
+ name = humanize_migration_name(migration[:name])
57
+ usage_text += "-- #{migration[:version]} - #{name}\n"
58
+ end
59
+ end
60
+
61
+ return usage_text
62
+ end
63
+ end
64
+
65
+ # Generate SQL for a single migration
66
+ def generate_migration_sql(migration_info)
67
+ begin
68
+ migration_class = @discoverer.load_migration_class(migration_info[:version])
69
+
70
+ if migration_class.nil?
71
+ return "-- ERROR: Could not load migration class for #{migration_info[:version]}"
72
+ end
73
+
74
+ # Intercept SQL at the connection execute level
75
+ captured_sql = []
76
+ connection = ActiveRecord::Base.connection
77
+
78
+ # Store original execute method
79
+ original_execute = connection.method(:execute)
80
+
81
+ # Override execute to capture SQL
82
+ connection.define_singleton_method(:execute) do |sql, name = nil|
83
+ captured_sql << sql.to_s
84
+ # Return empty result to prevent actual execution
85
+ ActiveRecord::Result.new([], [])
86
+ end
87
+
88
+ # Suppress Rails migration logging
89
+ original_verbose = ActiveRecord::Migration.verbose
90
+ original_logger = ActiveRecord::Base.logger
91
+
92
+ ActiveRecord::Migration.verbose = false
93
+ ActiveRecord::Base.logger = Logger.new(IO::NULL)
94
+
95
+ # Run the migration to generate SQL
96
+ migration_instance = migration_class.new
97
+ migration_instance.migrate(:up)
98
+
99
+ # Format captured SQL
100
+ if captured_sql.empty?
101
+ "-- No SQL statements captured for migration #{migration_info[:version]}"
102
+ else
103
+ captured_sql.map { |sql| format_sql_statement(sql) }.join("\n")
104
+ end
105
+
106
+ rescue => e
107
+ "-- ERROR in migration #{migration_info[:version]}: #{e.message}"
108
+ ensure
109
+ # Restore original execute method and logging
110
+ if connection && original_execute
111
+ connection.define_singleton_method(:execute, original_execute)
112
+ end
113
+
114
+ if defined?(original_verbose)
115
+ ActiveRecord::Migration.verbose = original_verbose
116
+ end
117
+
118
+ if defined?(original_logger)
119
+ ActiveRecord::Base.logger = original_logger
120
+ end
121
+ end
122
+ end
123
+
124
+ # Generate the schema_migrations INSERT statement
125
+ def generate_schema_migration_sql(version)
126
+ result_sql = nil
127
+ if defined?(ActiveRecord::SchemaMigration)
128
+ begin
129
+ # Create SchemaMigration instance with connection pool
130
+ pool = ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool
131
+ schema_migration = ActiveRecord::SchemaMigration.new(pool)
132
+
133
+ # Use the instance's arel_table and replicate Rails' create_version logic
134
+ arel_table = schema_migration.arel_table
135
+ insert_manager = Arel::InsertManager.new(arel_table)
136
+ insert_manager.insert(arel_table[schema_migration.primary_key] => version)
137
+ result_sql = insert_manager.to_sql
138
+ rescue => e
139
+ puts "-- Using default INSERT command after error generating schema migration SQL: #{e.message}."
140
+ result_sql = nil
141
+ end
142
+ else
143
+ result_sql = nil
144
+ end
145
+ if result_sql.nil?
146
+ # Fallback
147
+ result_sql = "INSERT INTO schema_migrations (version) VALUES ('#{version}')"
148
+ end
149
+ result_sql
150
+ end
151
+
152
+
153
+ private
154
+
155
+ def format_sql_statement(sql)
156
+ # Clean up and format SQL statement
157
+ formatted = sql.to_s.strip
158
+ formatted = formatted.gsub(/\s+/, ' ') # Normalize whitespace
159
+ formatted += ';' unless formatted.end_with?(';')
160
+ formatted
161
+ end
162
+
163
+ def format_migration_block(migration_info, migration_sql, schema_sql, format)
164
+ case format
165
+ when :commented
166
+ [
167
+ "-- Migration: #{migration_info[:version]} - #{migration_info[:name].humanize}",
168
+ "-- File: #{migration_info[:filename]}.rb",
169
+ migration_sql,
170
+ "",
171
+ "-- Update schema_migrations table:",
172
+ "#{schema_sql};"
173
+ ].join("\n")
174
+ else
175
+ [
176
+ migration_sql,
177
+ "#{schema_sql};"
178
+ ].join("\n")
179
+ end
180
+ end
181
+
182
+ def format_with_comments(sql_statements)
183
+ [
184
+ "-- Generated SQL for pending Rails migrations",
185
+ "-- Generated at: #{Time.current}",
186
+ "",
187
+ sql_statements.join("\n\n")
188
+ ].join("\n")
189
+ end
190
+
191
+ def humanize_migration_name(name)
192
+ # Convert migration name from snake_case to human readable
193
+ name.tr('_', ' ').split.map(&:capitalize).join(' ')
194
+ end
195
+
196
+ end
197
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Songbird
4
+ VERSION = "0.1.0"
5
+ end
data/lib/songbird.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "songbird/version"
4
+ require_relative "songbird/migration_discoverer"
5
+ require_relative "songbird/sql_generator"
6
+
7
+ if defined?(Rails)
8
+ require_relative "songbird/railtie"
9
+ end
10
+
11
+ module Songbird
12
+ class Error < StandardError; end
13
+
14
+ # Main entry point for the gem
15
+ def self.generate_sql(version: nil, migrations_path: nil, output_format: :raw)
16
+ generator = SQLGenerator.new(
17
+ migrations_path: migrations_path
18
+ )
19
+
20
+ generator.process(
21
+ version: version,
22
+ output_format: output_format
23
+ )
24
+ end
25
+
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'songbird'
4
+
5
+ namespace :db do
6
+ namespace :migrate do
7
+ desc 'Generate SQL for migrations'
8
+ task :sql, [:version, :migrations_path] => :environment do |task, args|
9
+ begin
10
+ # Create SQL generator
11
+ generator = Songbird::SQLGenerator.new(
12
+ migrations_path: args[:migrations_path]
13
+ )
14
+
15
+ version = args[:version]
16
+
17
+ sql_output = generator.process(
18
+ version: version,
19
+ output_format: :commented
20
+ )
21
+
22
+ puts sql_output
23
+
24
+ rescue => e
25
+ puts "Error generating migration SQL: #{e.message}"
26
+ puts e.backtrace.first(5).join("\n") if ENV['DEBUG']
27
+ exit 1
28
+ end
29
+ end
30
+
31
+
32
+ end
33
+ end
34
+
35
+ # Add helpful shortcuts
36
+ namespace :songbird do
37
+ desc 'Show SQL commands for migrations (shortcut)'
38
+ task :sql, [:version, :migrations_path] => 'db:migrate:sql'
39
+
40
+
41
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: songbird
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elcio Nakashima
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ description: Extract SQL statements from Rails migrations for manual review and execution.
56
+ Perfect for debugging, review, or manual execution of schema changes.
57
+ email:
58
+ - elciok@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".rspec"
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - lib/songbird.rb
68
+ - lib/songbird/migration_discoverer.rb
69
+ - lib/songbird/railtie.rb
70
+ - lib/songbird/sql_generator.rb
71
+ - lib/songbird/version.rb
72
+ - lib/tasks/songbird.rake
73
+ homepage: https://github.com/elciok/songbird
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ homepage_uri: https://github.com/elciok/songbird
78
+ source_code_uri: https://github.com/elciok/songbird
79
+ changelog_uri: https://github.com/elciok/songbird/blob/main/CHANGELOG.md
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 3.1.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.5.11
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Extract SQL statements from Rails migrations for manual review and execution.
99
+ test_files: []