├── spec ├── fake_app │ ├── config │ │ ├── application.rb │ │ ├── invalid_database.yml │ │ └── database.yml │ └── db │ │ ├── migrate │ │ ├── 20131205160936_first_migration.rb │ │ ├── 20140101010101_squasher_clean.rb │ │ ├── 20140103124257_third_migration.rb │ │ └── 20131213090719_second_migration.rb │ │ ├── structure.sql │ │ └── schema.rb ├── spec_helper.rb └── lib │ ├── cleaner_spec.rb │ ├── squasher_spec.rb │ ├── config_spec.rb │ └── worker_spec.rb ├── lib ├── squasher │ ├── version.rb │ ├── templates │ │ ├── init_schema.rb.erb │ │ └── squasher_clean.rb.erb │ ├── cleaner.rb │ ├── messages.yml │ ├── render.rb │ ├── worker.rb │ └── config.rb └── squasher.rb ├── Rakefile ├── Gemfile ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── LICENSE.txt ├── CHANGELOG.md ├── squasher.gemspec ├── bin └── squasher └── README.md /spec/fake_app/config/application.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fake_app/db/migrate/20131205160936_first_migration.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fake_app/db/migrate/20140101010101_squasher_clean.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fake_app/db/migrate/20140103124257_third_migration.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fake_app/db/migrate/20131213090719_second_migration.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/squasher/version.rb: -------------------------------------------------------------------------------- 1 | module Squasher 2 | VERSION = "0.8.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fake_app/config/invalid_database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | database: app_test 3 | user: root 4 | password: password 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new('spec') 5 | 6 | desc 'Run all specs' 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in squasher.gemspec 4 | gemspec 5 | 6 | eval(File.read('Gemfile.custom')) if File.exist?('Gemfile.custom') 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.bundle 4 | /.config 5 | /.rspec 6 | /dummy 7 | Gemfile.custom 8 | Gemfile.lock 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | -------------------------------------------------------------------------------- /lib/squasher/templates/init_schema.rb.erb: -------------------------------------------------------------------------------- 1 | class InitSchema < ActiveRecord::Migration<%= @config.migration_version %> 2 | def up 3 | <%- each_schema_line do |line| -%> 4 | <%- next if line.strip.empty? -%> 5 | <%= " #{line}" %> 6 | <%- end -%> 7 | end 8 | 9 | def down 10 | raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/squasher/templates/squasher_clean.rb.erb: -------------------------------------------------------------------------------- 1 | class SquasherClean < ActiveRecord::Migration<%= @config.migration_version %> 2 | class SchemaMigration < ActiveRecord::Base 3 | end 4 | 5 | def up 6 | migrations = Dir.glob(File.join(File.dirname(__FILE__), '*.rb')) 7 | versions = migrations.map { |file| File.basename(file)[/\A\d+/] } 8 | SchemaMigration.where("version NOT IN (?)", versions).delete_all 9 | end 10 | 11 | def down 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | module SpecHelpers 2 | def fake_root 3 | File.join(File.dirname(__FILE__), 'fake_app') 4 | end 5 | end 6 | 7 | module Squasher 8 | class Dir < ::Dir 9 | def self.pwd 10 | File.join(File.dirname(__FILE__), 'fake_app') 11 | end 12 | end 13 | 14 | def puts(*) 15 | end 16 | end 17 | 18 | require 'bundler/setup' 19 | Bundler.require 20 | 21 | RSpec.configure do |config| 22 | config.order = 'random' 23 | config.include SpecHelpers 24 | 25 | config.mock_with :rspec do |mocks| 26 | mocks.verify_partial_doubles = true 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/fake_app/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | database: app_development 3 | encoding: <%= "8-ftu".reverse %> 4 | user: root 5 | password: password 6 | test: 7 | database: app_test 8 | user: root 9 | password: password 10 | another_development: 11 | database: another_development 12 | encoding: <%= "8-ftu".reverse %> 13 | user: root 14 | password: password 15 | another_test: 16 | database: another_test 17 | user: root 18 | password: password 19 | multiverse-database: 20 | database: multiverse-database 21 | user: multiverse-user 22 | password: multiverse-password 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | specs: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby-version: 13 | - head 14 | - '3.3' 15 | - '3.2' 16 | - '3.1' 17 | - '3.0' 18 | - '2.7' 19 | - jruby 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Ruby ${{ matrix.ruby-version }} 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby-version }} 26 | bundler-cache: true # 'bundle install' and cache 27 | - name: Run tests 28 | run: bundle exec rake 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017 Sergey Pchelincev 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/lib/cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | describe Squasher::Cleaner do 5 | let(:expected_file) { File.join(fake_root, 'db', 'migrate', '20140102030405_squasher_clean.rb') } 6 | 7 | before do 8 | allow(Time).to receive(:now).and_return(Time.utc(2014, 1, 2, 3, 4, 5)) 9 | allow(Squasher).to receive(:rake).with("db:migrate", :db_cleaning) 10 | end 11 | 12 | after do 13 | FileUtils.rm(expected_file) 14 | end 15 | 16 | it 'create a new migration' do 17 | allow_any_instance_of(Squasher::Cleaner).to receive(:prev_migration).and_return(nil) 18 | Squasher.clean 19 | 20 | expect(clean_migrations).to include(expected_file) 21 | File.open(expected_file) do |stream| 22 | content = stream.read 23 | expect(content).to include("SquasherClean") 24 | end 25 | end 26 | 27 | it 'update an existing migration' do 28 | Squasher.clean 29 | 30 | expect(clean_migrations.size).to be(1) 31 | expect(clean_migrations.first).to eq(expected_file) 32 | 33 | FileUtils.touch(File.join(fake_root, 'db/migrate/20140101010101_squasher_clean.rb')) 34 | end 35 | 36 | def clean_migrations 37 | Dir.glob(File.join(fake_root, '**/*_squasher_clean.rb')) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - 0.8.0 2 | - Support multiple databases in Rails-style ([@fynsta](https://github.com/fynsta)) 3 | - 0.7.0 4 | - Support the presence of multiverse ([@mlohbihler](https://github.com/mlohbihler)) 5 | - 0.6.1 6 | - Escape regexp in sql mode ([@mpospelov](https://github.com/mpospelov)) 7 | - 0.6.0 8 | - Support apps with sql schema ([@mpospelov](https://github.com/mpospelov)) 9 | - 0.5.1 10 | - Fix work on Windows machines ([@tobmatth](https://github.com/tobmatth)) 11 | - 0.5.0 12 | - Rework command line integration. Fix clean process with Rails 5 13 | - 0.4.0 14 | - Support rails versioned migrations which were introduced in Rails 5 15 | - 0.3.1 16 | - fix init migration generation 17 | - 0.3.0 18 | - **rails engines support** ([@JakeTheSnake3p0](https://github.com/JakeTheSnake3p0)) 19 | - move messages from JSON file to YAML 20 | - allow to use a db config with a "soft" parsing errors 21 | - 0.2.2 22 | - strip white spaces in init migrations 23 | - 0.2.1 24 | - support rails 5 25 | - 0.2.0 26 | - add **dry** mode and ability to reuse the previous squasher database 27 | - improve database config processing 28 | - raise the minimum supported version of Ruby 29 | - 0.1.7 30 | - a regression fix of the log output ([@lime](https://github.com/lime)) 31 | - improve a multi-platform support ([@johncarney](https://github.com/johncarney)) 32 | - 0.1.6 33 | - support multiple database settings ([@ppworks](https://github.com/ppworks)) 34 | -------------------------------------------------------------------------------- /squasher.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'squasher/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "squasher" 8 | spec.version = Squasher::VERSION 9 | spec.authors = ["Sergey Pchelintsev"] 10 | spec.email = ["linz.sergey@gmail.com"] 11 | spec.description = <<-DESCRIPTION.chomp.gsub("\n", " ") 12 | Squasher compresses old ActiveRecord migrations. On a big project with 13 | many migrations, every rake db:migrate might take a few seconds, or creating 14 | of a new database might take a few minutes. That's because ActiveRecord loads 15 | all those migration files. Squasher removes all the migrations and creates a 16 | single migration with the final database state of the specified date. 17 | The new migration will look like a schema.rb file. 18 | DESCRIPTION 19 | spec.summary = %q{Squash your old migrations} 20 | spec.homepage = "https://github.com/jalkoby/squasher" 21 | spec.license = "MIT" 22 | 23 | spec.files = `git ls-files`.split($/) 24 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 25 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 26 | spec.require_paths = ["lib"] 27 | 28 | spec.add_development_dependency "bundler", ">= 1.3" 29 | spec.add_development_dependency "rake" 30 | spec.add_development_dependency "rspec", ">= 3.3.0" 31 | end 32 | -------------------------------------------------------------------------------- /spec/fake_app/db/structure.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE cities ( 2 | id integer NOT NULL, 3 | name character varying, 4 | created_at timestamp without time zone NOT NULL, 5 | updated_at timestamp without time zone NOT NULL, 6 | ); 7 | 8 | CREATE TABLE managers ( 9 | id integer NOT NULL, 10 | email character varying, 11 | password_digest character varying, 12 | created_at timestamp without time zone NOT NULL, 13 | updated_at timestamp without time zone NOT NULL, 14 | CONSTRAINT email_format CHECK (email ~* '^.+@.+\..+') 15 | ); 16 | 17 | CREATE TABLE offices ( 18 | id integer NOT NULL, 19 | name character varying, 20 | address character varying, 21 | phone character varying, 22 | description text, 23 | capacity integer, 24 | manager_id integer, 25 | city_id integer, 26 | created_at timestamp without time zone NOT NULL, 27 | updated_at timestamp without time zone NOT NULL 28 | ); 29 | 30 | CREATE TABLE ar_internal_metadata ( 31 | key character varying NOT NULL, 32 | value character varying, 33 | created_at timestamp without time zone NOT NULL, 34 | updated_at timestamp without time zone NOT NULL 35 | ); 36 | 37 | CREATE TABLE schema_migrations ( 38 | version character varying NOT NULL 39 | ); 40 | 41 | ALTER TABLE ONLY ar_internal_metadata 42 | ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); 43 | 44 | ALTER TABLE ONLY schema_migrations 45 | ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); 46 | 47 | INSERT INTO "schema_migrations" (version) VALUES 48 | ('20170831152134'), 49 | ('20170907145259'); 50 | -------------------------------------------------------------------------------- /lib/squasher/cleaner.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Squasher 4 | class Cleaner 5 | MIGRATION_NAME = 'squasher_clean' 6 | 7 | def self.process(*args) 8 | new(*args).process 9 | end 10 | 11 | def process 12 | Squasher.error(:migration_folder_missing) unless config.migrations_folders? 13 | 14 | if config.multi_db_format == 'rails' 15 | config.databases.each do |database| 16 | process_database(database) 17 | end 18 | else 19 | process_database 20 | end 21 | end 22 | 23 | def process_database(database = nil) 24 | migration_file = config.migration_file(now_timestamp, MIGRATION_NAME, database) 25 | if (prev_migration = prev_migration(database)) 26 | FileUtils.rm(prev_migration) 27 | end 28 | File.open(migration_file, 'wb') do |stream| 29 | stream << ::Squasher::Render.render(MIGRATION_NAME, config, database) 30 | end 31 | 32 | if database.nil? 33 | Squasher.rake("db:migrate", :db_cleaning) 34 | else 35 | Squasher.rake("db:migrate:#{database}", :db_cleaning) 36 | end 37 | end 38 | 39 | private 40 | 41 | def config 42 | Squasher.config 43 | end 44 | 45 | def prev_migration(database = nil) 46 | return @prev_migration if defined?(@prev_migration) 47 | 48 | @prev_migration = config.migration_files(database).detect do |file| 49 | File.basename(file).include?(MIGRATION_NAME) 50 | end 51 | end 52 | 53 | def now_timestamp 54 | Time.now.utc.strftime("%Y%m%d%H%M%S") 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/fake_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20130613090053) do 15 | 16 | create_table "cities", :force => true do |t| 17 | t.string "name" 18 | t.datetime "created_at", :null => false 19 | t.datetime "updated_at", :null => false 20 | end 21 | 22 | create_table "managers", :force => true do |t| 23 | t.string "email" 24 | t.string "password_digest" 25 | t.datetime "created_at", :null => false 26 | t.datetime "updated_at", :null => false 27 | end 28 | 29 | create_table "offices", :force => true do |t| 30 | t.string "name" 31 | t.string "address" 32 | t.string "phone" 33 | t.text "description" 34 | t.integer "capacity" 35 | t.datetime "created_at", :null => false 36 | t.datetime "updated_at", :null => false 37 | t.integer "manager_id" 38 | t.integer "city_id" 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /bin/squasher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | lib = File.expand_path('../../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'optparse' 5 | require 'squasher' 6 | 7 | options = {} 8 | parser = OptionParser.new do |config| 9 | config.banner = 'Supported options:' 10 | 11 | config.on('-d', '--dry', 'execute in "dry" mode(a test squashing without removing old migrations)') do 12 | options[:dry] = true 13 | end 14 | 15 | config.on('-s', '--sql', 'run squasher in the app with sql schema') do 16 | options[:sql] = true 17 | end 18 | 19 | config.on('-m', '--migration', '=VERSION', 'define the rails migration version(since Rails 5)') do |value| 20 | options[:migration] = value 21 | end 22 | 23 | config.on('-r', '--reuse', 'reuse the database from a previous squashing') do 24 | options[:reuse] = true 25 | end 26 | 27 | config.on('-e', 'run squasher in the engine(requires the dummy app inside)') do 28 | options[:engine] = nil 29 | end 30 | 31 | config.on('--engine=PATH', 'run squasher in the engine with a custom path to the dummy app') do |value| 32 | options[:engine] = value 33 | end 34 | 35 | config.on('--multi-db_format=FORMAT', 'format of the multi-db configuration (rails, multiverse)') do |value| 36 | options[:multi_db_format] = value 37 | end 38 | 39 | config.on('--databases=DB_KEY,...', 'alternate database configuration keys to be included') do |value| 40 | options[:databases] = value&.split(",") 41 | end 42 | 43 | config.on('-v', '--version', '-h', '--help', 'show this message') 44 | end 45 | parser.parse! 46 | 47 | Squasher.setup(options) 48 | case ARGV.first 49 | when /\A\d{4}/ then Squasher.squash(ARGV.first) 50 | when 'clean' then Squasher.clean 51 | else 52 | Squasher.tell(:usage, version: Squasher::VERSION) 53 | puts '' 54 | puts parser 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/squasher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Squasher do 4 | context '.squash' do 5 | specify { expected_covert("2013/12/23", Time.new(2013, 12, 23)) } 6 | specify { expected_covert("2013", Time.new(2013, 1, 1)) } 7 | 8 | def expected_covert(input, expected) 9 | expect(Squasher::Worker).to receive(:process).with(expected) 10 | Squasher.squash(input) 11 | end 12 | end 13 | 14 | context '.rake' do 15 | before do 16 | allow_any_instance_of(Object).to receive(:system) 17 | end 18 | 19 | context 'when given a description' do 20 | it 'outputs it' do 21 | expect(Squasher).to receive(:tell).with('description') 22 | Squasher.rake('db:migrate', 'description') 23 | end 24 | end 25 | 26 | it 'switches to the app root before running a given command' do 27 | allow(Dir).to receive(:pwd) { fake_root } 28 | expect(Dir).to receive(:chdir).with(fake_root) 29 | Squasher.rake('db:migrate') 30 | end 31 | 32 | it 'runs a given command' do 33 | expect_any_instance_of(Object).to receive(:system).with(anything, /db:migrate/) 34 | Squasher.rake('db:migrate') 35 | end 36 | 37 | it 'sets the Rails environment to development' do 38 | expect_any_instance_of(Object).to receive(:system) 39 | .with(hash_including('RAILS_ENV' => 'development'), /db:migrate/) 40 | Squasher.rake('db:migrate') 41 | end 42 | 43 | it 'disables database environment check' do 44 | expect_any_instance_of(Object).to receive(:system) 45 | .with(hash_including('DISABLE_DATABASE_ENVIRONMENT_CHECK' => '1'), /db:migrate/) 46 | Squasher.rake('db:migrate') 47 | end 48 | end 49 | 50 | context '.print' do 51 | context 'when options are empty' do 52 | it 'prints the message without error' do 53 | Squasher.print('asdf % asdf', {}) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/squasher.rb: -------------------------------------------------------------------------------- 1 | module Squasher 2 | extend self 3 | 4 | autoload :Cleaner, 'squasher/cleaner' 5 | autoload :Config, 'squasher/config' 6 | autoload :Render, 'squasher/render' 7 | autoload :VERSION, 'squasher/version' 8 | autoload :Worker, 'squasher/worker' 9 | 10 | attr_reader :config 11 | 12 | @config = Config.new 13 | 14 | def setup(options) 15 | options.each { |(k, v)| config.set(k, v) } 16 | end 17 | 18 | def squash(raw_date) 19 | parts = raw_date.to_s.split('/').map(&:to_i) 20 | date = Time.new(*parts) 21 | Worker.process(date) 22 | end 23 | 24 | def clean 25 | Cleaner.process 26 | end 27 | 28 | def rake(command, description = nil) 29 | tell(description) if description 30 | config.in_app_root do 31 | env = { 'RAILS_ENV' => 'development', 'DISABLE_DATABASE_ENVIRONMENT_CHECK' => '1' } 32 | system(env, "bundle exec rake #{ command }") 33 | end 34 | end 35 | 36 | def ask(*args) 37 | tell(*args) 38 | $stdin.gets[0].downcase == 'y' 39 | end 40 | 41 | def tell(key, options = {}) 42 | message = messages.fetch(key.to_s) 43 | message = message.join("\n") if message.is_a?(Array) 44 | message = colorize(message) 45 | print(message, options) 46 | end 47 | 48 | def print(message, options = {}) 49 | puts options.empty? ? message : message % options 50 | end 51 | 52 | def error(*args) 53 | tell(*args) 54 | abort 55 | end 56 | 57 | private 58 | 59 | def messages 60 | return @messages if @messages 61 | 62 | require 'yaml' 63 | path = File.join(File.dirname(__FILE__), 'squasher/messages.yml') 64 | @messages = YAML.load(File.open(path)) 65 | end 66 | 67 | COLORS = ['red', 'green', 'yellow', 'blue'].each_with_index.inject({}) { |r, (k, i)| r.merge!(k => "03#{ i + 1 }") } 68 | 69 | def colorize(message) 70 | message.gsub(/\:(\w+)\<([^>]+)\>/) { |_| "\033[#{ COLORS[$1] }m#{ $2 }\033[039m" } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/squasher/messages.yml: -------------------------------------------------------------------------------- 1 | keep_database: 2 | - "Squasher's created the `:green` database for its needs." 3 | - It might be useful to keep it if any of your deleted migrations inserts data or 4 | - you squash migrations in a few steps (look at -r option). 5 | - "Keep it (:green / :red)?" 6 | apply_clean: "Do you want to clean your database from the old schema migration records(:red/:green)?" 7 | migration_folder_missing: 8 | - The folder with migrations is missing. 9 | - "Are you sure that you're in the :red application?" 10 | no_migrations: "There are no migrations in the folder prior to :red<%{date}>" 11 | use_dbconfig: "There were problems in parsing `:green`. Do you want to use the config below (:green/:red)?\n\n%{config}\n" 12 | dbconfig_invalid: "Squasher couldn't load `:green`. Please, make sure that it's present and doesn't include any ruby code." 13 | db_create: Squasher is creating a tmp database 14 | db_migrate: Squasher is applying migrations on a tmp database 15 | db_cleaning: Squasher is applying the clean migration 16 | db_reuse: Squasher is reusing the database from the previous squashing 17 | usage: 18 | - "Squasher %{version}" 19 | - "" 20 | - "Examples of usage:" 21 | - " :green [options] :yellow squash migrations prior to a specified date" 22 | - " :green [options] :yellow generate or update a cleaning migration and apply it" 23 | 24 | wrong_option: "You provided a wrong option :red<%{arg}>\n\n" 25 | invalid_param: "You provided a wrong param :red<%{arg}>\n\n" 26 | dry_mode_finished: "The `dry` mode is finished. Below the init schema's content:" 27 | cannot_find_dummy: "Squasher can't find a dummy app inside :yellow<%{base}>" 28 | multi_dummy_case: "Squasher found a few config/application.rb inside :yellow<%{base}>. Please specify the path more precisely" 29 | invalid_migration_version: The provided rails migration version `%{value}` is invalid. It should be like `5.0` 30 | -------------------------------------------------------------------------------- /lib/squasher/render.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module Squasher 4 | class Render 5 | def self.render(*args) 6 | new(*args).render 7 | end 8 | 9 | attr_reader :name, :config 10 | 11 | def initialize(name, config, database = nil) 12 | @name = name 13 | @config = config 14 | @database = database 15 | end 16 | 17 | def render 18 | if RUBY_VERSION < '2.6' 19 | ERB.new(template("#{ name }.rb"), nil, '-').result(binding) 20 | else 21 | ERB.new(template("#{ name }.rb"), trim_mode: '-').result(binding) 22 | end 23 | end 24 | 25 | def each_schema_line(&block) 26 | File.open(config.schema_file(@database), 'r') do |stream| 27 | if @config.set?(:sql) 28 | stream_structure(stream, &block) 29 | else 30 | stream_schema(stream, &block) 31 | end 32 | end 33 | end 34 | 35 | private 36 | 37 | def stream_structure(stream) 38 | yield 'execute %q{' 39 | skip_mode = false 40 | ignored_table = ['ar_internal_metadata', 'schema_migrations'] 41 | stream.each_line do |line| 42 | skip_mode = true if ignored_table.any? { |t| line.include?(t) } 43 | 44 | if skip_mode 45 | skip_mode = false if line.include?(';') 46 | next 47 | end 48 | 49 | yield line.gsub(/\A\s{,2}(.*)\s+\z/, '\1') 50 | end 51 | yield '}' 52 | end 53 | 54 | def stream_schema(stream) 55 | inside_schema = false 56 | 57 | stream.each_line do |raw_line| 58 | if inside_schema 59 | # reach the end of schema 60 | break if raw_line.index("end") == 0 61 | line = raw_line.gsub(/\A\s{,2}(.*)\s+\z/, '\1') 62 | if line.include?('create_table') 63 | line.gsub!(/(create_table.*),.* do/, '\1 do') 64 | end 65 | yield line 66 | else 67 | inside_schema = true if raw_line.include?("ActiveRecord::Schema") 68 | end 69 | end 70 | end 71 | 72 | def template(name) 73 | path = File.join(File.dirname(__FILE__), "templates/#{ name }.erb") 74 | template = File.open(path, "rb") 75 | content = template.read 76 | template.close 77 | content 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Squasher::Config do 4 | let(:config) { described_class.new } 5 | 6 | context '.dbconfig?' do 7 | subject(:result) { config.dbconfig? } 8 | 9 | it 'a file is exists and it has a valid content' do 10 | expect(result).to be_truthy 11 | end 12 | 13 | it 'a file is exists but doesnt have a valid content' do 14 | allow(config).to receive(:dbconfig_file).and_return(File.join(fake_root, 'config', 'invalid_database.yml')) 15 | 16 | expect(result).to be_falsey 17 | end 18 | 19 | it 'a file is not exists' do 20 | allow(config).to receive(:dbconfig_file).and_return(File.join(fake_root, 'config', 'not_existed.yml')) 21 | 22 | expect(result).to be_falsey 23 | end 24 | end 25 | 26 | context '#stub_dbconfig' do 27 | it 'add required db config file' do 28 | config.stub_dbconfig do 29 | File.open(File.join(fake_root, 'config', 'database.yml')) do |stream| 30 | content = YAML.load(stream.read) 31 | expect(content["development"]["database"]).to eq("squasher") 32 | expect(content["development"]["encoding"]).to eq("utf-8") 33 | expect(content).not_to have_key("another_development") 34 | end 35 | end 36 | end 37 | 38 | it 'recover original schema and db config files if some error raised' do 39 | begin 40 | config.stub_dbconfig do 41 | expect(file_exists?('config', 'database.yml')).to be_truthy 42 | expect(file_exists?('config', 'database.yml.sq')).to be_truthy 43 | 44 | raise RuntimeError, "Unexpected system error" 45 | end 46 | rescue RuntimeError 47 | expect(file_exists?('config', 'database.yml')).to be_truthy 48 | expect(file_exists?('config', 'database.yml.sq')).to be_falsey 49 | end 50 | end 51 | 52 | it 'includes other database definitions provided in the command line' do 53 | config.set(:databases, ["multiverse-database"]) 54 | config.stub_dbconfig do 55 | File.open(File.join(fake_root, 'config', 'database.yml')) do |stream| 56 | content = YAML.load(stream.read) 57 | expect(content["development"]["database"]).to eq("squasher") 58 | expect(content["development"]["encoding"]).to eq("utf-8") 59 | expect(content["multiverse-database"]["database"]).to eq("multiverse-database") 60 | expect(content["multiverse-database"]["user"]).to eq("multiverse-user") 61 | expect(content).not_to have_key("another_development") 62 | end 63 | end 64 | end 65 | 66 | def file_exists?(*parts) 67 | File.exist?(File.join(fake_root, *parts)) 68 | end 69 | end 70 | 71 | specify { expect(config.migration_file(1230, :sample)).to eq(File.join(fake_root, 'db', 'migrate', '1230_sample.rb')) } 72 | 73 | specify "generate versioned migrations" do 74 | config = Squasher::Config.new 75 | config.set(:migration, '5.1') 76 | content = Squasher::Render.render(:init_schema, config) 77 | expect(content).to include('ActiveRecord::Migration[5.1]') 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/squasher/worker.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Squasher 4 | class Worker 5 | attr_reader :date 6 | 7 | def self.process(*args) 8 | new(*args).process 9 | end 10 | 11 | def initialize(date) 12 | @date = date 13 | end 14 | 15 | def process 16 | check! 17 | 18 | result = under_squash_env do 19 | if Squasher.config.set?(:dry) 20 | Squasher.tell(:dry_mode_finished) 21 | Squasher.print(Render.render(:init_schema, config)) 22 | else 23 | if config.multi_db_format == 'rails' 24 | config.databases.each do |database| 25 | clean_migrations(database) 26 | end 27 | else 28 | clean_migrations 29 | end 30 | end 31 | 32 | Squasher.rake("db:drop") unless Squasher.ask(:keep_database) 33 | end 34 | 35 | Squasher.clean if result && Squasher.ask(:apply_clean) 36 | end 37 | 38 | private 39 | 40 | def config 41 | Squasher.config 42 | end 43 | 44 | def check! 45 | Squasher.error(:migration_folder_missing) unless config.migrations_folders? 46 | Squasher.error(:dbconfig_invalid) unless config.dbconfig? 47 | 48 | if config.multi_db_format == 'rails' 49 | config.databases.each do |database| 50 | check_migrations_exist(database) 51 | end 52 | else 53 | check_migrations_exist 54 | end 55 | end 56 | 57 | def check_migrations_exist(database = nil) 58 | if migrations(database).empty? 59 | print_date = date.strftime("%Y/%m/%d") 60 | 61 | Squasher.error(:no_migrations, :date => date.strftime("%Y/%m/%d")) 62 | end 63 | end 64 | 65 | def migrations(database = nil) 66 | config.migration_files(database).select { |file| before_date?(get_timestamp(file)) }.sort 67 | end 68 | 69 | def get_timestamp(file) 70 | File.basename(file)[/\A\d+/] 71 | end 72 | 73 | def clean_migrations(database = nil) 74 | path = config.migration_file(finish_timestamp(database), :init_schema, database) 75 | migrations(database).each { |file| FileUtils.rm(file) } # Remove all migrations before creating the new one 76 | File.open(path, 'wb') { |io| io << Render.render(:init_schema, config, database) } 77 | end 78 | 79 | def before_date?(timestamp) 80 | @point ||= date.strftime("%Y%m%d").to_i 81 | return unless timestamp 82 | timestamp[0...8].to_i < @point 83 | end 84 | 85 | def finish_timestamp(database = nil) 86 | get_timestamp(migrations(database).last) 87 | end 88 | 89 | def under_squash_env 90 | config.stub_dbconfig do 91 | if Squasher.config.set?(:reuse) 92 | Squasher.tell(:db_reuse) 93 | else 94 | return unless Squasher.rake("db:drop db:create", :db_create) 95 | end 96 | 97 | if config.multi_db_format == 'rails' 98 | config.databases.each do |database| 99 | return unless Squasher.rake("db:migrate:#{ database } VERSION=#{ finish_timestamp(database) }", :db_migrate) 100 | end 101 | else 102 | return unless Squasher.rake("db:migrate VERSION=#{ finish_timestamp }", :db_migrate) 103 | end 104 | 105 | yield 106 | 107 | true 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squasher 2 | 3 | [![CI](https://github.com/jalkoby/squasher/actions/workflows/ci.yml/badge.svg)](https://github.com/jalkoby/squasher/actions/workflows/ci.yml) 4 | [![Code Climate](https://codeclimate.com/github/jalkoby/squasher.svg)](https://codeclimate.com/github/jalkoby/squasher) 5 | [![Gem Version](https://badge.fury.io/rb/squasher.svg)](http://badge.fury.io/rb/squasher) 6 | 7 | Squasher compresses old ActiveRecord migrations. If you work on a big project with lots of migrations, every `rake db:migrate` might take a few seconds, or creating of a new database might take a few minutes. That's because ActiveRecord loads all those migration files. Squasher removes all the migrations and creates a single migration with the final database state of the specified date (the new migration will look like a schema). 8 | 9 | ## Attention 10 | Prior to 0.6.2 squasher could damage your real data as generate "force" tables. Please upgrade to 0.6.2+ & manually clean "force" tag from the init migration 11 | 12 | ## Installation 13 | 14 | You don't have to add it into your Gemfile. Just a standalone installation: 15 | 16 | $ gem install squasher 17 | 18 | **@note** if you use Rbenv don't forget to run `rbenv rehash`. 19 | 20 | If you want to share it with your rails/sinatra/etc app add the below: 21 | 22 | ```ruby 23 | # Yep, the missing group in most Gemfiles where all utilities should be! 24 | group :tools do 25 | gem 'squasher', '>= 0.6.0' 26 | gem 'capistrano' 27 | gem 'rubocop' 28 | end 29 | ``` 30 | 31 | Don't forget to run `bundle`. 32 | 33 | To integrate `squasher` with your app even more do the below: 34 | 35 | $ bundle binstub squasher 36 | $ # and you have a runner inside the `bin` folder 37 | $ bin/squasher 38 | 39 | ## Usage 40 | 41 | **@note** stop all preloading systems if there are present (spring, zeus, etc) 42 | 43 | Suppose your application was created a few years ago. `%app_root%/db/migrate` folder looks like this: 44 | ```bash 45 | 2012...._first_migration.rb 46 | 2012...._another_migration.rb 47 | # and a lot of other files 48 | 2013...._adding_model_foo.rb 49 | # few years later 50 | 2016...._removing_model_foo.rb 51 | # and so on 52 | ``` 53 | 54 | Storing these atomic changes over time is painful and useless. It's time to archive this history. Once you install the gem you can run the `squasher` command. For example, you want to compress all migrations which were created prior to the year 2017: 55 | 56 | $ squasher 2017 # rails 3 & 4 57 | $ squasher 2017 -m 5.0 # rails 5+ 58 | 59 | You can tell `squasher` a more detailed date, for example: 60 | 61 | $ squasher 2016/12 # prior to December 2016 62 | $ squasher 2016/12/19 # prior to 19 December 2016 63 | 64 | ### Options 65 | 66 | Run `squasher -h` or just `squasher` to see how you can use squasher: 67 | 68 | - in sql schema rails app 69 | - in rails 5+ app 70 | - inside an engine 71 | - in "dry" mode 72 | - in "reuse" mode 73 | 74 | ## Requirements 75 | 76 | It works and was tested on Ruby 2.0+ and ActiveRecord 3.1+. It also requires a valid development configuration in `config/database.yml`. 77 | If an old migration inserted data (created ActiveRecord model records) you will lose this code in the squashed migration, **BUT** `squasher` will ask you to leave a tmp database which will have all data that was inserted while migrating. Using this database you could add that data as another migration, or into `config/seed.rb` (the expected place for this stuff). 78 | 79 | ## Changelog 80 | 81 | All changes are located in [the changelog file](CHANGELOG.md) with contribution notes 82 | 83 | ## Contributing 84 | 85 | 1. Fork it 86 | 2. Create your feature branch (`git checkout -b my-new-feature`) 87 | 3. Commit your changes (`git commit -am 'Add some feature'`) 88 | 4. Push to the branch (`git push origin my-new-feature`) 89 | 5. Create new Pull Request 90 | -------------------------------------------------------------------------------- /spec/lib/worker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | describe Squasher::Worker do 5 | context 'failed on #check!' do 6 | let(:worker) { described_class.new(Time.new(2012, 6, 20)) } 7 | 8 | specify 'command was run not in application root' do 9 | allow_any_instance_of(Squasher::Config).to receive(:migrations_folders?).and_return(false) 10 | 11 | expect_exit_with(:migration_folder_missing) 12 | end 13 | 14 | specify 'db configuration is invalid' do 15 | allow_any_instance_of(Squasher::Config).to receive(:dbconfig?).and_return(false) 16 | 17 | expect_exit_with(:dbconfig_invalid) 18 | end 19 | 20 | specify 'matched migrations was not found' do 21 | expect_exit_with(:no_migrations, :date => "2012/06/20") 22 | end 23 | 24 | def expect_exit_with(*args) 25 | expect(Squasher).to receive(:error).with(*args).and_call_original 26 | expect { worker.process }.to raise_error(SystemExit) 27 | end 28 | end 29 | 30 | it 'creates a new squashed migration & remove selected migrations' do 31 | worker = described_class.new(Time.new(2014)) 32 | allow(worker).to receive(:under_squash_env).and_yield.and_return(true) 33 | new_migration_path = File.join(Dir.tmpdir, 'init_schema.rb') 34 | allow_any_instance_of(Squasher::Config).to receive(:migration_file).with('20131213090719', :init_schema, nil).and_return(new_migration_path) 35 | 36 | expect(FileUtils).to receive(:rm).with(File.join(fake_root, 'db', 'migrate', '20131205160936_first_migration.rb')) 37 | expect(FileUtils).to receive(:rm).with(File.join(fake_root, 'db', 'migrate', '20131213090719_second_migration.rb')) 38 | expect(Squasher).to receive(:ask).with(:keep_database).and_return(false) 39 | expect(Squasher).to receive(:rake).with("db:drop") 40 | expect(Squasher).to receive(:ask).with(:apply_clean).and_return(true) 41 | expect(Squasher).to receive(:clean) 42 | 43 | worker.process 44 | 45 | expect(File.exist?(new_migration_path)).to be_truthy 46 | File.open(new_migration_path) do |stream| 47 | content = stream.read 48 | expect(content).to include("InitSchema") 49 | expect(content).to include('create_table "managers" do |t|') 50 | end 51 | end 52 | 53 | context 'with flags' do 54 | before do 55 | Squasher.instance_variable_set(:@config, Squasher::Config.new) 56 | end 57 | 58 | specify 'the sql mode' do 59 | Squasher.config.set(:sql, true) 60 | worker = described_class.new(Time.new(2014)) 61 | allow(worker).to receive(:under_squash_env).and_yield.and_return(true) 62 | new_migration_path = File.join(Dir.tmpdir, 'init_schema.rb') 63 | allow_any_instance_of(Squasher::Config).to receive(:migration_file).with('20131213090719', :init_schema, nil).and_return(new_migration_path) 64 | expect(FileUtils).to receive(:rm).with(File.join(fake_root, 'db', 'migrate', '20131205160936_first_migration.rb')) 65 | expect(FileUtils).to receive(:rm).with(File.join(fake_root, 'db', 'migrate', '20131213090719_second_migration.rb')) 66 | 67 | expect(Squasher).to receive(:ask).with(:keep_database).and_return(false) 68 | expect(Squasher).to receive(:rake).with("db:drop") 69 | expect(Squasher).to receive(:ask).with(:apply_clean).and_return(false) 70 | worker.process 71 | 72 | expect(File.exist?(new_migration_path)).to be_truthy 73 | File.open(new_migration_path) do |stream| 74 | content = stream.read 75 | expect(content.strip).to eq(%q{ 76 | class InitSchema < ActiveRecord::Migration 77 | def up 78 | execute %q{ 79 | CREATE TABLE cities ( 80 | id integer NOT NULL, 81 | name character varying, 82 | created_at timestamp without time zone NOT NULL, 83 | updated_at timestamp without time zone NOT NULL, 84 | ); 85 | CREATE TABLE managers ( 86 | id integer NOT NULL, 87 | email character varying, 88 | password_digest character varying, 89 | created_at timestamp without time zone NOT NULL, 90 | updated_at timestamp without time zone NOT NULL, 91 | CONSTRAINT email_format CHECK (email ~* '^.+@.+\..+') 92 | ); 93 | CREATE TABLE offices ( 94 | id integer NOT NULL, 95 | name character varying, 96 | address character varying, 97 | phone character varying, 98 | description text, 99 | capacity integer, 100 | manager_id integer, 101 | city_id integer, 102 | created_at timestamp without time zone NOT NULL, 103 | updated_at timestamp without time zone NOT NULL 104 | ); 105 | } 106 | end 107 | 108 | def down 109 | raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable" 110 | end 111 | end 112 | }.strip 113 | ) 114 | end 115 | end 116 | 117 | specify 'the dry mode' do 118 | Squasher.config.set(:dry, nil) 119 | worker = described_class.new(Time.new(2014)) 120 | allow(worker).to receive(:under_squash_env).and_yield.and_return(true) 121 | expect(Squasher).to receive(:tell).with(:dry_mode_finished).and_call_original 122 | expect(Squasher).to receive(:ask).with(:keep_database).and_return(false) 123 | expect(Squasher).to receive(:rake).with("db:drop") 124 | expect(Squasher).to receive(:ask).with(:apply_clean).and_return(false) 125 | worker.process 126 | end 127 | 128 | specify 'reuse of an existing database' do 129 | Squasher.config.set(:reuse, nil) 130 | worker = described_class.new(Time.new(2014)) 131 | expect(Squasher).to receive(:tell).with(:db_reuse).and_call_original 132 | expect(Squasher).not_to receive(:rake).with("db:drop db:create", :db_create) 133 | allow(Squasher).to receive(:rake).with("db:migrate VERSION=20131213090719", :db_migrate).and_return(false) 134 | worker.process 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/squasher/config.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'yaml' 3 | require 'erb' 4 | 5 | module Squasher 6 | class Config 7 | module Render 8 | extend self 9 | 10 | def process(path) 11 | @error = false 12 | # Support for Psych 4 (the default yaml parser for Ruby 3.1) 13 | opts = Gem::Version.new(Psych::VERSION).segments.first < 4 ? {} : { aliases: true } 14 | str = YAML.load(ERB.new(File.read(path)).result(binding), **opts) 15 | [str, @error] 16 | end 17 | 18 | def method_missing(*args) 19 | @error = true 20 | self 21 | end 22 | 23 | def const_missing(*args) 24 | @error = true 25 | self 26 | end 27 | 28 | def to_s 29 | '' 30 | end 31 | 32 | def inspect 33 | '' 34 | end 35 | end 36 | 37 | attr_reader :migration_version, :multi_db_format, :databases 38 | 39 | def initialize 40 | @root_path = Dir.pwd.freeze 41 | @flags = [] 42 | @multi_db_format = nil 43 | @databases = [] 44 | set_app_path(@root_path) 45 | end 46 | 47 | def set(key, value) 48 | if key == :engine 49 | base = value.nil? ? @root_path : File.expand_path(value, @root_path) 50 | list = Dir.glob(File.join(base, '**', '*', 'config', 'application.rb')) 51 | case list.size 52 | when 1 53 | set_app_path(File.expand_path('../..', list.first)) 54 | when 0 55 | Squasher.error(:cannot_find_dummy, base: base) 56 | else 57 | Squasher.error(:multi_dummy_case, base: base) 58 | end 59 | elsif key == :migration 60 | Squasher.error(:invalid_migration_version, value: value) unless value.to_s =~ /\A\d.\d\z/ 61 | @migration_version = "[#{value}]" 62 | elsif key == :multi_db_format 63 | Squasher.error(:invalid_multi_db_format, value: value) unless %w[rails multiverse].include?(value) 64 | @multi_db_format = value 65 | elsif key == :databases 66 | @databases = value 67 | else 68 | @flags << key 69 | end 70 | end 71 | 72 | def set?(k) 73 | @flags.include?(k) 74 | end 75 | 76 | def schema_files 77 | return [schema_file] unless @multi_db_format == 'rails' 78 | 79 | @databases.map { |db| schema_file(db) } 80 | end 81 | 82 | def schema_file(database = nil) 83 | prefix = database.nil? || database == 'primary' ? '' : "#{ database }_" 84 | file = set?(:sql) ? 'structure.sql' : 'schema.rb' 85 | 86 | File.join(@app_path, 'db', "#{ prefix }#{ file }") 87 | end 88 | 89 | def migration_files(database = nil) 90 | Dir.glob(File.join(migrations_folder(database), '**.rb')) 91 | end 92 | 93 | def migration_file(timestamp, migration_name, database = nil) 94 | File.join(migrations_folder(database), "#{ timestamp }_#{ migration_name }.rb") 95 | end 96 | 97 | def migrations_folder(database = nil) 98 | return default_migration_folder if database.nil? 99 | 100 | migrations_paths = dbconfig['development'][database]['migrations_paths'] 101 | return default_migration_folder unless migrations_paths 102 | 103 | File.join(@app_path, migrations_paths) 104 | end 105 | 106 | def migrations_folders? 107 | if @multi_db_format != 'rails' 108 | Dir.exist?(migrations_folder) 109 | else 110 | @databases.all? { |db| Dir.exist?(migrations_folder(db)) } 111 | end 112 | end 113 | 114 | def default_migration_folder 115 | File.join(@root_path, 'db', 'migrate') 116 | end 117 | 118 | def dbconfig? 119 | !dbconfig.nil? 120 | end 121 | 122 | def stub_dbconfig 123 | return unless dbconfig? 124 | 125 | list = [dbconfig_file, *schema_files] 126 | list.each do |file| 127 | next unless File.exist?(file) 128 | FileUtils.mv file, "#{ file }.sq" 129 | end 130 | 131 | File.open(dbconfig_file, 'wb') { |stream| stream.write dbconfig.to_yaml } 132 | 133 | yield 134 | 135 | ensure 136 | list.each do |file| 137 | next unless File.exist?("#{ file }.sq") 138 | FileUtils.mv "#{ file }.sq", file 139 | end 140 | end 141 | 142 | def in_app_root(&block) 143 | Dir.chdir(@app_path, &block) 144 | end 145 | 146 | private 147 | 148 | attr_reader :dbconfig_file 149 | 150 | def dbconfig 151 | return @dbconfig if defined?(@dbconfig) 152 | return @dbconfig = nil unless File.exist?(dbconfig_file) 153 | 154 | @dbconfig = nil 155 | 156 | begin 157 | content, soft_error = Render.process(dbconfig_file) 158 | if content.has_key?('development') 159 | if @multi_db_format == 'rails' 160 | @dbconfig = { 'development' => {} } 161 | @databases.each do |database| 162 | @dbconfig['development'][database] = content['development'][database].merge('database' => "#{database}_squasher") 163 | 164 | database_name = content['development'][database]['database'] 165 | content['development'].select { |_, v| v['database'] == database_name && v['replica'] }.each do |k, v| 166 | @dbconfig['development'][k] = v.merge('database' => "#{database}_squasher") 167 | end 168 | end 169 | else 170 | @dbconfig = { 'development' => content['development'].merge('database' => 'squasher') } 171 | 172 | multiverse_by_default = @multi_db_format.nil? && @databases.any? 173 | if multiverse_by_default 174 | puts "Using multiverse format by default is deprecated and will be removed in the next major release. Please specify --multi-db_format=rails or --multi-db_format=multiverse explicitly." 175 | end 176 | if multiverse_by_default || @multi_db_format == 'multiverse' 177 | @databases&.each { |database| @dbconfig[database] = content[database] } 178 | end 179 | end 180 | end 181 | rescue 182 | end 183 | 184 | if soft_error && @dbconfig 185 | exit unless Squasher.ask(:use_dbconfig, config: @dbconfig.fetch('development')) 186 | end 187 | @dbconfig 188 | end 189 | 190 | def set_app_path(path) 191 | @app_path = path 192 | @dbconfig_file = File.join(path, 'config', 'database.yml') 193 | end 194 | end 195 | end 196 | --------------------------------------------------------------------------------