├── .gitignore ├── .rspec ├── .rvmrc ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── sequel_tools.rb └── sequel_tools │ ├── actions │ ├── before_task.rb │ ├── connect_db.rb │ ├── create_db.rb │ ├── down.rb │ ├── drop_db.rb │ ├── irb.rb │ ├── migrate.rb │ ├── new_migration.rb │ ├── postgresql_support.rb │ ├── redo.rb │ ├── reset.rb │ ├── rollback.rb │ ├── schema_dump.rb │ ├── schema_dump_postgres.rb │ ├── schema_load.rb │ ├── seed.rb │ ├── setup.rb │ ├── shell.rb │ ├── shell_postgres.rb │ ├── status.rb │ ├── up.rb │ └── version.rb │ ├── actions_manager.rb │ ├── all_actions.rb │ ├── migration_utils.rb │ ├── pg_helper.rb │ ├── sequel_tools_logger.rb │ └── version.rb ├── scripts └── ci │ └── travis-build.sh ├── sequel_tools.gemspec └── spec ├── actions ├── connect_db_spec.rb ├── create_drop_db_spec.rb ├── irb_spec.rb ├── migrate_and_schema_load_and_setup_spec.rb ├── new_migration_spec.rb ├── reset_spec.rb ├── schema_dump_spec.rb ├── seed_spec.rb ├── shell_spec.rb └── up_down_redo_rollback_version_status_spec.rb ├── fast_rake_runner.rb ├── hooks_spec.rb ├── log_level_spec.rb ├── rake_exec_runner.rb ├── rake_runner.rb ├── sample_project ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── Rakefile.custom-shell ├── Rakefile.hooks ├── Rakefile.no_pg_schema_dump └── custom-shell ├── sequel_tools_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /spec/sample_project/db/migrations/ 10 | /spec/sample_project/db/seeds.rb 11 | /spec/sample_project/bin/bundle 12 | /spec/sample_project/bin/sequel 13 | /Gemfile.lock 14 | 15 | # rspec failure tracking 16 | .rspec_status 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | # fix warnings in Travis CI build caused by RVM using the -client option in JRUBY_OPTS 2 | unset JRUBY_OPTS 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: ruby 3 | rvm: 4 | - 3.0.1 5 | #- jruby-9.2.17.0 6 | before_install: scripts/ci/travis-build.sh 7 | #before_install: gem install bundler -v 1.16.0 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | #git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in sequel_tools.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Rodrigo Rosenfeld Rosas 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SequelTools [![Build Status](https://travis-ci.org/rosenfeld/sequel_tools.svg?branch=master)](https://travis-ci.org/rosenfeld/sequel_tools) 2 | 3 | SequelTools brings some tooling around Sequel migrations and database management, providing tasks 4 | to create, drop and migrate the database, plus dumping and restoring from the last migrated schema. 5 | It can also display which migrations are applied and which ones are missing. It's highly 6 | customizable and supports multiple databases or environments. It integrates well with Rake as well. 7 | 8 | Currently only PostgreSQL is supported out-of-the-box for some tasks, but it should allow you to 9 | specify the database vendor specific commands to support your vendor of choice without requiring 10 | changes to SequelTools itself. Other vendors can be supported through additional gems or you may 11 | want to submit pull requests for your preferred DB vendor if you prefer so that it would be 12 | supported out-of-the-box by this gem. 13 | 14 | The idea behind SequelTools is to create a collection of supported actions, which depend on the 15 | database adapter/vendor. Those supported actions can then be translated to Rake tasks as a possible 16 | interface. 17 | 18 | ## Installation 19 | 20 | Add this line to your application's Gemfile: 21 | 22 | ```ruby 23 | gem 'sequel_tools' 24 | gem 'rake' 25 | 26 | # For PostgreSQL: 27 | gem 'pg', platform: :mri 28 | gem 'jdbc-postgres', platform: :jruby 29 | ``` 30 | 31 | And then execute: 32 | 33 | bundle 34 | 35 | ## Usage 36 | 37 | Here's a sample Rakefile supporting migrate actions: 38 | 39 | ```ruby 40 | require 'bundler/setup' 41 | require 'sequel_tools' 42 | 43 | base_config = { 44 | # project_root: Dir.pwd, 45 | dbadapter: 'postgres', 46 | dbname: 'mydb', 47 | username: 'myuser', 48 | password: 'secret', 49 | # default log_level is nil, in which mode the executed actions such as 50 | # starting/finishing a migration in a given direction or creating and 51 | # dropping the database are not logged to standard output. 52 | log_level: :info, 53 | 54 | # Default options: 55 | sql_log_level: :debug, 56 | dump_schema_on_migrate: false, # it's a good idea to enable it for the reference environment 57 | pg_dump: 'pg_dump', # command used to run pg_dump 58 | pg_dump: 'psql', # command used to run psql when calling rake db:shell if adapter is postgres 59 | migrations_location: 'db/migrations', 60 | schema_location: 'db/migrations/schema.sql', 61 | seeds_location: 'db/seeds.rb', 62 | # for tasks such as creating the database: 63 | # when nil, defaults to the value of the :dbadapter config. 64 | # This is the database we should connect to before executing "create database dbname" 65 | maintenancedb: :default, 66 | # migrations_table: 'schema_migrations', 67 | # allow other tables data to be included in the dump file generated by rake db:schema_dump 68 | # extra_tables_in_dump: nil 69 | # for example, if you want to keep migrations from ActiveRecord in the dump file, while using 70 | # another table for Sequel migrations: 71 | # migrations_table: 'sequel_schema_migrations', 72 | # extra_tables_in_dump: ['schema_migrations'], 73 | } 74 | 75 | namespace 'db' do 76 | SequelTools.inject_rake_tasks base_config.merge(dump_schema_on_migrate: true), self 77 | end 78 | 79 | namespace 'dbtest' do 80 | SequelTools.inject_rake_tasks base_config.merge(dbname: 'mydb_test'), self 81 | end 82 | ``` 83 | 84 | Then you are able to run several tasks (`rake -T` will list all supported): 85 | 86 | rake db:create 87 | rake db:new_migration[migration_name] 88 | rake db:migrate 89 | # setup creates the database, loads the latest schema 90 | # and import seeds when available 91 | rake dbtest:setup 92 | # reset drops (if existing) then recreate the database, run all migrations 93 | # and import seeds when available 94 | rake dbtest:reset 95 | # shell opens a psql section to the database for the PostgreSQL adapter 96 | rake db:shell 97 | # irb runs "bundle exec sequel" pointing to the database and stores the connection in "DB" 98 | rake db:irb 99 | rake db:rollback 100 | rake db:status 101 | # version displays latest applied migration 102 | rake db:version 103 | rake db:seed 104 | rake db:redo[migration_filename] 105 | rake db:down[migration_filename] 106 | rake db:up[migration_filename] 107 | # schema_dump is called automatically after migrate/rollback/up/down/redo 108 | # if passing { dump_schema_on_migrate: true } to the config 109 | rake db:schema_dump 110 | # database must be empty before calling db:schema_load 111 | rake db:schema_load 112 | 113 | You may define your own command to open a shell to your database upon the 'db:shell' task. 114 | PostgreSQL is supported out-of-the-box, but if it wasn't, here's a sample script that would 115 | get the job done: 116 | 117 | ```bash 118 | #!/bin/bash 119 | 120 | # name it like ~/bin/opensql for example and give it execution permission 121 | 122 | PGDATABASE=$DBNAME 123 | PGHOST=$DBHOST 124 | PGPORT=$DBPORT 125 | PGUSER=$DBUSERNAME 126 | PGPASSWORD=$DBPASSWORD 127 | psql 128 | ``` 129 | 130 | Then you may set `shell_command: '~/bin/opensql'` in config. 131 | 132 | Alternatively you can define the `shell_#{dbadapter}` action if you prefer. Take a look at 133 | the implementation for `shell_postgres` to see how to do that. If you want to share that action 134 | with others you may either submit a pull request to this project or create a separate gem to 135 | add support for your database to `sequel_tools`, which wouldn't require waiting for me to 136 | approve your pull requests and you'd be able to maintain it independently. 137 | 138 | ## Development and running tests 139 | 140 | The tests assume the database `sequel_tools_test_pw` exists and can be only accessed using a 141 | username and password. It also assumes a valid user/passwd is `sequel_tools_user/secret`. The 142 | database `sequel_tools_test` is also required to exist and it should be possible to access it 143 | using the `trust` authentication method, without requiring a password. You may achieve that by 144 | adding these lines to the start of your `pg_hba.conf`: 145 | 146 | ``` 147 | host sequel_tools_test_pw all 127.0.0.1/32 md5 148 | host sequel_tools_test all 127.0.0.1/32 trust 149 | ``` 150 | 151 | Then feel free to run the tests: 152 | 153 | bundle exec rspec 154 | 155 | The default strategy is a safe one, which uses `Open3.capture3` to actually run 156 | `bundle exec rake ...` whenever we want to test the Rake integration and we do that many times 157 | in the tests. Running `bundle exec` is slow, so it adds a lot to the test suite total execution 158 | time. Alternatively, although less robust, you may run the tests using a fork-rake approach, 159 | which avoids calling `bundle exec` each time we want to run a Rake task. Just define 160 | the `FORK_RAKE` environment variable: 161 | 162 | FORK_RAKE=1 bundle exec rspec 163 | 164 | In my environment this would complete the full suite in about half the time. 165 | 166 | ## Contributing 167 | 168 | Bug reports and pull requests are welcome on GitHub at 169 | [https://github.com/rosenfeld/sequel_tools](https://github.com/rosenfeld/sequel_tools). 170 | 171 | ## License 172 | 173 | The gem is available as open source under the terms of the 174 | [MIT License](https://opensource.org/licenses/MIT). 175 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "sequel_tools" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/sequel_tools.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel_tools/version' 4 | 5 | module SequelTools 6 | DEFAULT_CONFIG = { 7 | project_root: Dir.pwd, 8 | pg_dump: 'pg_dump', # command used to run pg_dump 9 | psql: 'psql', # command used to run psql 10 | maintenancedb: :default, # DB to connect to for creating/dropping databases 11 | migrations_location: 'db/migrations', 12 | schema_location: 'db/migrations/schema.sql', 13 | seeds_location: 'db/seeds.rb', 14 | dbname: nil, 15 | dbhost: nil, 16 | dbadapter: 'postgres', 17 | dbport: nil, 18 | username: nil, 19 | password: nil, 20 | dump_schema_on_migrate: false, 21 | log_level: nil, 22 | sql_log_level: :debug, 23 | migrations_table: nil, 24 | extra_tables_in_dump: nil, 25 | } # unfrozen on purpose so that one might want to update the defaults 26 | 27 | REQUIRED_KEYS = [ :dbadapter, :dbname ] 28 | class MissingConfigError < StandardError; end 29 | def self.base_config(extra_config = {}) 30 | config = DEFAULT_CONFIG.merge extra_config 31 | REQUIRED_KEYS.each do |key| 32 | raise MissingConfigError, "Expected value for #{key} config is missing" if config[key].nil? 33 | end 34 | [:migrations_location, :schema_location, :seeds_location].each do |k| 35 | config[k] = File.expand_path config[k], config[:project_root] 36 | end 37 | config 38 | end 39 | 40 | def self.inject_rake_tasks(config = {}, rake_context) 41 | require_relative 'sequel_tools/actions_manager' 42 | require_relative 'sequel_tools/all_actions' 43 | actions_manager = ActionsManager.new base_config(config) 44 | actions_manager.load_all 45 | actions_manager.export_as_rake_tasks rake_context 46 | end 47 | 48 | def self.suppress_java_output 49 | return yield unless RUBY_PLATFORM == 'java' 50 | require 'java' 51 | require 'stringio' 52 | old_err = java.lang.System.err 53 | java.lang.System.err = java.io.PrintStream.new(StringIO.new.to_outputstream) 54 | yield 55 | ensure 56 | java.lang.System.err = old_err if RUBY_PLATFORM == 'java' 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/before_task.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | SequelTools::ActionsManager::Action.register :before_task, nil do |args, context| 6 | config = context[:config] 7 | adapter = config[:dbadapter] 8 | action = context[:current_action] 9 | hooks = [:before_any, :"before_#{action.name}", :"before_any_#{adapter}", 10 | :"before_#{action.name}_#{adapter}"] 11 | hooks.each do |h| 12 | next unless a = SequelTools::ActionsManager::Action[h] 13 | a.run args, context 14 | context[:"#{h}_processed"] = true 15 | end 16 | config[:maintenancedb] = adapter if context[:maintenancedb] == :default 17 | end 18 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/connect_db.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | require_relative '../sequel_tools_logger' 6 | 7 | SequelTools::ActionsManager::Action.register :connect_db, nil do |args, context| 8 | next if context[:db] 9 | config = context[:config] 10 | context[:db] = db = SequelTools.suppress_java_output do 11 | Sequel.connect context[:uri_builder].call(config), test: true 12 | end 13 | db.sql_log_level = config[:sql_log_level] 14 | db.log_connection_info = false 15 | next unless log_level = config[:log_level] 16 | require 'logger' 17 | db.logger = SequelTools::SequelToolsLogger.new(STDOUT, log_level) 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/create_db.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | 6 | class SequelTools::ActionsManager 7 | Action.register :create, 'Create database' do |args, context| 8 | begin 9 | Action[:connect_db].run({}, context) 10 | rescue 11 | c = context[:config] 12 | db = Sequel.connect context[:uri_builder].call(c, c[:maintenancedb]) 13 | db << "create database #{c[:dbname]}" 14 | db.disconnect 15 | Action[:connect_db].run({}, context) 16 | context[:db].log_info "Created database '#{c[:dbname]}'" 17 | else 18 | puts 'Database already exists - aborting' 19 | exit 1 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/down.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | require_relative '../migration_utils' 6 | 7 | class SequelTools::ActionsManager 8 | desc = 'Run specified migration down if not already applied' 9 | Action.register :down, desc, arg_names: [ :version ] do |args, context| 10 | MigrationUtils.apply_migration context, args[:version], :down 11 | Action[:schema_dump].run({}, context) if context[:config][:dump_schema_on_migrate] 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/drop_db.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | 6 | class SequelTools::ActionsManager 7 | Action.register :drop, 'Drop database' do |args, context| 8 | begin 9 | Action[:connect_db].run({}, context) 10 | rescue 11 | puts 'Database does not exist - aborting.' 12 | exit 1 13 | else 14 | (old_db = context.delete :db).disconnect 15 | c = context[:config] 16 | db = Sequel.connect context[:uri_builder].call(c, c[:maintenancedb]) 17 | db << "drop database #{c[:dbname]}" 18 | db.disconnect 19 | old_db.log_info "Dropped database '#{c[:dbname]}'" 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/irb.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | description = "Opens an IRB session started as 'sequel uri_to_database' (DB is available to irb)" 7 | Action.register :irb, description do |args, context| 8 | # This code does the job, but for some reason the test will timeout under JRuby 9 | #config = context[:config] 10 | #uri = context[:uri_builder][config] 11 | #exec "bundle exec sequel #{uri}" 12 | 13 | Action[:connect_db].run({}, context) 14 | require 'irb' 15 | ::DB = context[:db] 16 | ARGV.clear 17 | puts 'Your database is stored in DB...' 18 | IRB.start 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/migrate.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | 6 | class SequelTools::ActionsManager 7 | desc = 'Migrate to specified version (last by default)' 8 | Action.register :migrate, desc, arg_names: [ :version ] do |args, context| 9 | Action[:connect_db].run({}, context) 10 | db = context[:db] 11 | config = context[:config] 12 | Sequel.extension :migration unless Sequel.respond_to? :migration 13 | options = {} 14 | options[:target] = args[:version].to_i if args[:version] 15 | options[:table] = config[:migrations_table] if config[:migrations_table] 16 | Sequel::Migrator.run db, config[:migrations_location], options 17 | Action[:schema_dump].run({}, context) if config[:dump_schema_on_migrate] 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/new_migration.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | desc = 'Creates a new migration' 7 | Action.register :new_migration, desc, arg_names: [ :name ] do |args, context| 8 | (puts 'Migration name is missing - aborting'; exit 1) unless name = args[:name] 9 | require 'time' 10 | migrations_path = context[:config][:migrations_location] 11 | filename = "#{migrations_path}/#{Time.now.strftime '%Y%m%d%H%M%S'}_#{name}.rb" 12 | File.write filename, <<~MIGRATIONS_TEMPLATE_END 13 | # documentation available at http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html 14 | Sequel.migration do 15 | change do 16 | # create_table(:table_name) do 17 | # primary_key :id 18 | # String :name, null: false 19 | # end 20 | end 21 | # or call up{} and down{} 22 | end 23 | MIGRATIONS_TEMPLATE_END 24 | puts "The new migration file was created under #{filename.inspect}" 25 | end 26 | end 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/postgresql_support.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | require_relative 'schema_dump_postgres' 5 | require_relative 'shell_postgres' 6 | 7 | class SequelTools::ActionsManager 8 | Action.register :before_any_postgres, nil do |args, context| 9 | next if context[:before_any_postgres_processed] 10 | config = context[:config] 11 | config[:maintenancedb] = 'postgres' if config[:maintenancedb] == :default 12 | config[:jdbc_adapter] = 'postgresql' if RUBY_PLATFORM == 'java' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/redo.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | 6 | class SequelTools::ActionsManager 7 | desc = 'Run specified migration down and up (redo)' 8 | Action.register :redo, desc, arg_names: [ :version ] do |args, context| 9 | Action[:down].run(args, context) 10 | Action[:up].run(args, context) 11 | Action[:schema_dump].run({}, context) if context[:config][:dump_schema_on_migrate] 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/reset.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | desc = 'Recreate database from scratch by running all migrations and seeds in a new database' 7 | Action.register :reset, desc, arg_names: [ :version ] do |args, context| 8 | begin 9 | Action[:drop].run({}, context) 10 | rescue 11 | end 12 | Action[:create].run({}, context) 13 | Action[:migrate].run({}, context) 14 | Action[:seed].run({}, context) 15 | end 16 | end 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/rollback.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | require_relative '../migration_utils' 6 | 7 | class SequelTools::ActionsManager 8 | Action.register :rollback, 'Rollback last applied migration' do |args, context| 9 | unless last_found_migration = MigrationUtils.last_found_migration(context) 10 | puts 'No existing migrations are applied - cannot rollback' 11 | exit 1 12 | end 13 | MigrationUtils.apply_migration context, last_found_migration, :down 14 | Action[:schema_dump].run({}, context) if context[:config][:dump_schema_on_migrate] 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/schema_dump.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | Action.register :schema_dump, 'Store current db schema' do |args, context| 7 | begin 8 | Action[:connect_db].run({}, context) 9 | rescue 10 | puts 'Database does not exist - aborting.' 11 | exit 1 12 | else 13 | c = context[:config] 14 | unless action = Action[:"schema_dump_#{c[:dbadapter]}"] 15 | puts "Dumping the db schema is not currently supported for #{c[:dbadapter]}. Aborting" 16 | exit 1 17 | end 18 | action.run({}, context) 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/schema_dump_postgres.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | require_relative '../pg_helper' 5 | 6 | class SequelTools::ActionsManager 7 | Action.register :schema_dump_postgres, nil do |args, context| 8 | c = context[:config] 9 | pg_dump = c[:pg_dump] 10 | schema_location = c[:schema_location] 11 | 12 | stdout, stderr, success = PgHelper.run_pg_command c, "#{pg_dump} -s" 13 | return unless success 14 | content = stdout 15 | migrations_table = c[:migrations_table] 16 | if (migrations_table ? content.include?(migrations_table) : 17 | (content =~ /schema_(migrations|info)/)) 18 | include_tables = migrations_table ? [migrations_table] : 19 | ['schema_migrations', 'schema_info'] 20 | extra_tables = c[:extra_tables_in_dump] 21 | include_tables.concat extra_tables if extra_tables 22 | table_options = include_tables.map{|t| "-t #{t}"}.join(' ') 23 | stdout, stderr, success = 24 | PgHelper.run_pg_command c, "#{pg_dump} -a #{table_options} --inserts" 25 | unless success 26 | puts 'failed to dump data for schema_migrations and schema_info. Aborting.' 27 | exit 1 28 | end 29 | content = [content, stdout].join "\n\n" 30 | end 31 | require 'fileutils' 32 | FileUtils.mkdir_p File.dirname schema_location 33 | File.write schema_location, content 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/schema_load.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | description = 'Populates an empty database with previously dumped schema' 7 | Action.register :schema_load, description do |args, context| 8 | begin 9 | Action[:connect_db].run({}, context) 10 | rescue 11 | puts 'Database does not exist - aborting.' 12 | exit 1 13 | else 14 | schema_location = context[:config][:schema_location] 15 | unless File.exist? schema_location 16 | puts "Schema file '#{schema_location}' does not exist. Aborting." 17 | exit 1 18 | end 19 | context[:db].run File.read(schema_location) 20 | context[:db].disconnect # forces reconnection 21 | end 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/seed.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | Action.register :seed, 'Load seeds from seeds.rb' do |args, context| 7 | begin 8 | Action[:connect_db].run({}, context) 9 | rescue 10 | puts 'Database does not exist - aborting.' 11 | exit 1 12 | else 13 | if File.exist?(seeds_location = context[:config][:seeds_location]) 14 | ::DB = context[:db] 15 | load seeds_location 16 | end 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | description = 'Creates and populates a database with previously dumped schema and seeds' 7 | Action.register :setup, description do |args, context| 8 | begin 9 | Action[:connect_db].run({}, context) 10 | rescue 11 | Action[:create].run({}, context) 12 | Action[:schema_load].run({}, context) 13 | Action[:seed].run({}, context) 14 | else 15 | puts 'Database already exists - aborting.' 16 | exit 1 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/shell.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | Action.register :shell, 'Open an interactive shell to the database' do |args, context| 7 | begin 8 | Action[:connect_db].run({}, context) 9 | rescue 10 | puts 'Database does not exist - aborting.' 11 | exit 1 12 | else 13 | c = context[:config] 14 | if shell_command = c[:shell_command] 15 | env = { 16 | 'DBHOST' => c[:dbhost], 'DBPORT' => c[:dbport].to_s, 'DBUSERNAME' => c[:username], 17 | 'DBPASSWORD' => c[:password].to_s, 'DBNAME' => c[:dbname] 18 | } 19 | exec env, shell_command 20 | else 21 | unless action = Action[:"shell_#{c[:dbadapter]}"] 22 | puts "Opening an interactive shell is not currently supported for #{c[:dbadapter]}." + 23 | " Aborting" 24 | exit 1 25 | end 26 | action.run({}, context) 27 | end 28 | end 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/shell_postgres.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../actions_manager' 4 | 5 | class SequelTools::ActionsManager 6 | Action.register :shell_postgres, nil do |args, context| 7 | c = context[:config] 8 | psql = c[:psql] 9 | env = { 10 | 'PGDATABASE' => c[:dbname], 11 | 'PGHOST' => c[:dbhost] || 'localhost', 12 | 'PGPORT' => (c[:dbport] || 5432).to_s, 13 | 'PGUSER' => c[:username], 14 | 'PGPASSWORD' => c[:password].to_s, 15 | } 16 | exec env, psql 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/status.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | require_relative '../migration_utils' 6 | 7 | class SequelTools::ActionsManager 8 | description = 'Show migrations status (applied and missing in migrations path vs unapplied)' 9 | Action.register :status, description do |args, context| 10 | unapplied, files_missing = MigrationUtils.migrations_differences context 11 | path = context[:config][:migrations_location] 12 | unless files_missing.empty? 13 | puts "The following migrations were applied to the database but can't be found in #{path}:" 14 | puts files_missing.map{|fn| " - #{fn}" }.join("\n") 15 | puts 16 | end 17 | if unapplied.empty? 18 | puts 'No pending migrations in the database' if files_missing.empty? 19 | else 20 | puts 'Unapplied migrations:' 21 | puts unapplied.map{|fn| " - #{fn}" }.join("\n") 22 | puts 23 | end 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/up.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | require_relative '../migration_utils' 6 | 7 | class SequelTools::ActionsManager 8 | desc = 'Run specified migration up if not already applied' 9 | Action.register :up, desc, arg_names: [ :version ] do |args, context| 10 | MigrationUtils.apply_migration context, args[:version], :up 11 | Action[:schema_dump].run({}, context) if context[:config][:dump_schema_on_migrate] 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions/version.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require_relative '../actions_manager' 5 | require_relative '../migration_utils' 6 | 7 | class SequelTools::ActionsManager 8 | Action.register :version, 'Displays current version' do |args, context| 9 | puts MigrationUtils.current_version(context) || 'No migrations applied' 10 | end 11 | end 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/sequel_tools/actions_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module SequelTools 4 | class ActionsManager 5 | attr_reader :actions, :context 6 | 7 | URI_BUILDER = ->(config, dbname = config[:dbname]) do 8 | c = config 9 | uri_parts = [] 10 | uri_parts << 'jdbc:' if RUBY_PLATFORM == 'java' 11 | uri_parts << "#{c[:jdbc_adapter] || c[:dbadapter]}://" 12 | host = c[:dbhost] 13 | host ||= 'localhost' unless (c[:dbadapter] == 'sqlite') || c[:no_dbhost] 14 | if host 15 | uri_parts << host 16 | uri_parts << ':' << c[:dbport] if c[:dbport] 17 | uri_parts << '/' 18 | end 19 | uri_parts << dbname 20 | if user = c[:username] 21 | uri_parts << '?user=' << user 22 | uri_parts << '&password=' << c[:password] if c[:password] 23 | end 24 | uri_parts.join('') 25 | end 26 | 27 | def initialize(config) 28 | @actions = [] 29 | @context = { config: config, uri_builder: URI_BUILDER } 30 | end 31 | 32 | def load_all 33 | @actions.concat Action.registered 34 | end 35 | 36 | def export_as_rake_tasks(rake_context) 37 | actions.each do |action| 38 | ctx = context 39 | rake_context.instance_eval do 40 | desc action.description unless action.description.nil? 41 | task action.name, action.arg_names do |t, args| 42 | require_relative 'actions/before_task' 43 | ctx[:current_action] = action 44 | Action[:before_task].run args, ctx 45 | action.run args, ctx 46 | end 47 | end 48 | end 49 | end 50 | 51 | class Action 52 | attr_reader :name, :description, :arg_names, :block 53 | 54 | def initialize(name, description, arg_names: [], &block) 55 | @name, @description, @arg_names, @block = name, description, arg_names, block 56 | end 57 | 58 | def run(args, context) 59 | @block.call args, context 60 | end 61 | 62 | @@registered = [] 63 | @@registered_by_name = {} 64 | class AlreadyRegisteredAction < StandardError; end 65 | def self.register(name, description, arg_names: [], &block) 66 | if @@registered_by_name[name] 67 | raise AlreadyRegisteredAction, "Attempt to register #{name} twice" 68 | end 69 | @@registered << 70 | (@@registered_by_name[name] = Action.new name, description, arg_names: arg_names, &block) 71 | end 72 | 73 | def self.registered 74 | @@registered 75 | end 76 | 77 | def self.[](name) 78 | @@registered_by_name[name] 79 | end 80 | 81 | def self.unregister(name) 82 | return unless action = @@registered_by_name.delete(name) 83 | @@registered.delete action 84 | action 85 | end 86 | 87 | def self.replace(name, description, arg_names: [], &block) 88 | unregister name 89 | register name, description, arg_names: arg_names, &block 90 | end 91 | 92 | def self.register_action(action) 93 | register action.name, action.description, arg_names: action.arg_names, &action.block 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/sequel_tools/all_actions.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative 'actions/connect_db' 4 | require_relative 'actions/create_db' 5 | require_relative 'actions/drop_db' 6 | require_relative 'actions/schema_dump' 7 | require_relative 'actions/schema_load' 8 | require_relative 'actions/new_migration' 9 | require_relative 'actions/migrate' 10 | require_relative 'actions/reset' 11 | require_relative 'actions/setup' 12 | require_relative 'actions/seed' 13 | require_relative 'actions/up' 14 | require_relative 'actions/down' 15 | require_relative 'actions/redo' 16 | require_relative 'actions/version' 17 | require_relative 'actions/rollback' 18 | require_relative 'actions/status' 19 | require_relative 'actions/shell' 20 | require_relative 'actions/irb' 21 | require_relative 'actions/postgresql_support' 22 | -------------------------------------------------------------------------------- /lib/sequel_tools/migration_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative 'actions_manager' 4 | require 'sequel' 5 | 6 | class MigrationUtils 7 | def self.apply_migration(context, version, direction) 8 | ( puts 'migration version is missing - aborting.'; exit 1 ) if version.nil? 9 | filename = "#{File.basename version, '.rb'}.rb" 10 | migrator = find_migrator context, direction 11 | migrator.migration_tuples.delete_if{|(m, fn, dir)| fn != filename } 12 | unless (size = migrator.migration_tuples.size) == 1 13 | puts "Expected a single unapplied migration for #{filename} but found #{size}. Aborting." 14 | exit 1 15 | end 16 | migrator.run 17 | end 18 | 19 | def self.find_migrator(context, direction = :up) 20 | Sequel.extension :migration unless Sequel.respond_to? :migration 21 | SequelTools::ActionsManager::Action[:connect_db].run({}, context) unless context[:db] 22 | options = { allow_missing_migration_files: true } 23 | options[:target] = 0 if direction == :down 24 | config = context[:config] 25 | options[:table] = config[:migrations_table] if config[:migrations_table] 26 | Sequel::Migrator.migrator_class(config[:migrations_location]). 27 | new(context[:db], config[:migrations_location], options) 28 | end 29 | 30 | def self.current_version(context) 31 | migrator = find_migrator(context) 32 | migrator.ds.order(Sequel.desc(migrator.column)).get migrator.column 33 | end 34 | 35 | def self.last_found_migration(context) 36 | migrations_path = context[:config][:migrations_location] 37 | migrator = find_migrator(context) 38 | migrator.ds.order(Sequel.desc(migrator.column)).select_map(migrator.column).find do |fn| 39 | File.exist?("#{migrations_path}/#{fn}") 40 | end 41 | end 42 | 43 | def self.migrations_differences(context) 44 | config = context[:config] 45 | migrations_path = config[:migrations_location] 46 | existing = Dir["#{migrations_path}/*.rb"].map{|fn| File.basename(fn).downcase }.sort 47 | existing.delete(File.basename(config[:seeds_location])&.downcase) 48 | migrator = find_migrator context 49 | migrated = migrator.ds.select_order_map(migrator.column) 50 | unapplied = existing - migrated 51 | files_missing = migrated - existing 52 | [ unapplied, files_missing ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/sequel_tools/pg_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | class PgHelper 4 | def self.run_pg_command(config, cmd, connect_database: nil) 5 | require 'open3' 6 | require 'tempfile' 7 | Tempfile.open 'pgpass' do |file| 8 | c = config 9 | file.chmod 0600 10 | host = c[:dbhost] || 'localhost' 11 | port = c[:dbport] || '5432' 12 | file.write "#{host}:#{port}:#{c[:dbname]}:#{c[:username]}:#{c[:password]}" 13 | file.close 14 | env = { 15 | 'PGDATABASE' => connect_database || c[:dbname], 16 | 'PGHOST' => host, 17 | 'PGPORT' => port.to_s, 18 | 'PGUSER' => c[:username], 19 | 'PGPASSFILE' => file.path 20 | } 21 | stdout, stderr, status = Open3.capture3 env, cmd 22 | puts "#{cmd} failed: #{[stderr, stdout].join "\n\n"}" if status != 0 23 | [ stdout, stderr, status == 0 ] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/sequel_tools/sequel_tools_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'logger' 4 | 5 | module SequelTools 6 | class SequelToolsLogger < Logger 7 | def initialize(logdev, level) 8 | super logdev 9 | self.level = level 10 | self.formatter = proc do |severity, datetime, progname, msg| 11 | "[#{severity}] #{msg}\n" 12 | end 13 | end 14 | 15 | def add(severity, message = nil, progname = nil, &block) 16 | message = block_given? ? yield : progname if message.nil? 17 | return if severity == ERROR && 18 | message =~ /relation "schema_(migrations|info)" does not exist/ 19 | super 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/sequel_tools/version.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module SequelTools 4 | VERSION = '0.1.14' 5 | end 6 | -------------------------------------------------------------------------------- /scripts/ci/travis-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script enable PG 9.6, creates the sequel_tools_test_pw database, which can be only 4 | # accessed using a password through localhost, and the sequel_tools_test database which can be 5 | # accessed using the trust method. It also creates the sequel_tools_user user with password 6 | # 'secret'. This way we can test accessing the database both using a username and password or 7 | # other methods defined by pg_hba.conf, such as trust, which doesn't require a password. 8 | 9 | sudo /etc/init.d/postgresql stop 10 | PGVERSION=9.6 11 | PGHBA=/etc/postgresql/$PGVERSION/main/pg_hba.conf 12 | sudo bash -c "sed -i '1i host sequel_tools_test_pw all 127.0.0.1/32 md5' $PGHBA" 13 | sudo /etc/init.d/postgresql start $PGVERSION 14 | psql -c "create user sequel_tools_user superuser password 'secret'" postgres 15 | psql -U sequel_tools_user -c "create database sequel_tools_test" postgres 16 | psql -U sequel_tools_user -c "create database sequel_tools_test_pw" postgres 17 | -------------------------------------------------------------------------------- /sequel_tools.gemspec: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'sequel_tools/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'sequel_tools' 9 | spec.version = SequelTools::VERSION 10 | spec.authors = ['Rodrigo Rosenfeld Rosas'] 11 | spec.email = ['rr.rosas@gmail.com'] 12 | 13 | spec.summary = %q{Add Rake tasks to manage Sequel migrations} 14 | spec.description = <<~DESCRIPTION_END 15 | Offer tooling for common database operations, such as running Sequel migrations, store and 16 | load from the database schema, rollback, redo some migration, rerun all migrations, display 17 | applied and missing migrations. It allows integration with Rake, while being lazily evaluated 18 | to not slow down other Rake tasks. It's also configurable in order to support more actions 19 | and database vendors. Some tasks are currently implemented for PostgreSQL only for the time 20 | being out of the box. It should be possible to complement this gem with another one to 21 | support other database vendors, while taking advantage of the build blocks provided by this 22 | tool set. 23 | DESCRIPTION_END 24 | spec.homepage = 'https://github.com/rosenfeld/sequel_tools' 25 | spec.license = 'MIT' 26 | 27 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 28 | f.match(%r{^spec/}) 29 | end 30 | #spec.bindir = 'exe' 31 | #spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ['lib'] 33 | 34 | spec.add_runtime_dependency 'sequel' 35 | spec.add_development_dependency 'pg' unless RUBY_PLATFORM == 'java' 36 | spec.add_development_dependency 'jdbc-postgres' if RUBY_PLATFORM == 'java' 37 | spec.add_development_dependency 'bundler' 38 | spec.add_development_dependency 'rake' 39 | spec.add_development_dependency 'rspec' 40 | end 41 | -------------------------------------------------------------------------------- /spec/actions/connect_db_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | RSpec.describe 'connect_db action' do 4 | 5 | before(:all){ drop_test_database_if_exists } 6 | 7 | it 'connects to sequel_tools_test successfully' do 8 | expect(rake_runner.run_task('db:connect_db')).to be_successful 9 | end 10 | 11 | it 'connects to sequel_tools_test_pw successfully' do 12 | expect(rake_runner.run_task('dbpw:connect_db')).to be_successful 13 | end 14 | 15 | it 'fails to connect to sequel_tools_test_test when it does not exist' do 16 | expect(rake_runner.run_task('dbtest:connect_db')).to_not be_successful 17 | end 18 | 19 | it 'connects to sequel_tools_test_test when it exists' do 20 | db << 'create database sequel_tools_test_test' 21 | expect(rake_runner.run_task('dbtest:connect_db')).to be_successful 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/actions/create_drop_db_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | RSpec.describe 'create and drop actions', order: :defined do 4 | before(:all){ drop_test_database_if_exists } 5 | 6 | it 'creates sequel_tools_test_test successfully when it does not exist upon dbtest:create' do 7 | expect(rake_runner.run_task('dbtest:connect_db')).to_not be_successful 8 | expect(rake_runner.run_task('dbtest:create dbtest:connect_db')).to be_successful 9 | end 10 | 11 | it 'aborts if database already exist upon dbtest:create' do 12 | # second attempt fails because it already exists, examples are ordered in this context 13 | expect(rake_runner.run_task('dbtest:create')).to_not be_successful 14 | end 15 | 16 | it 'drops database if it exists on dbtest:drop' do 17 | expect(rake_runner.run_task('dbtest:drop')).to be_successful 18 | end 19 | 20 | it 'ignore drop database request if it does not exist on dbtest:drop' do 21 | expect(rake_runner.run_task('dbtest:drop')).to_not be_successful 22 | end 23 | 24 | it 'logs actions in verbose mode' do 25 | create_result = rake_runner.run_task 'dbtestverbose:create' 26 | expect(create_result).to be_successful 27 | expect(create_result.stdout).to eq "[INFO] Created database 'sequel_tools_test_test'\n" 28 | drop_result = rake_runner.run_task 'dbtestverbose:drop' 29 | expect(drop_result).to be_successful 30 | expect(drop_result.stdout).to eq "[INFO] Dropped database 'sequel_tools_test_test'\n" 31 | end 32 | 33 | it 'does not log actions unless in verbose mode' do 34 | result = rake_runner.run_task 'dbtest:create dbtest:drop' 35 | expect(result).to be_successful 36 | expect(result.stdout).to be_empty 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /spec/actions/irb_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | RSpec.describe 'irb action' do 4 | # TODO: why isn't this test working under JRuby? The rake task seems to work though 5 | # and the test for the shell action is pretty similar and works with JRuby... 6 | # After recent changes in newer Rubies, this test is no longer passing on MRI. 7 | # Writing to the pipe doesn't seem to be working for irb 8 | #it 'opens a sql console to the database' do 9 | # expected = /"sequel_tools_test"/ 10 | # expect{ 11 | # rake_exec_runner.wait_output 'db:irb', 'DB["select current_database()"].get', expected 12 | # }.to_not raise_exception 13 | #end 14 | 15 | # so, let's only half test it for now, to make sure at least it displays a message 16 | # saying that the DB is available to the shell 17 | it 'opens a sql console to the database' do 18 | expected = /Your database is stored in DB\.\.\./ 19 | expect{ 20 | rake_exec_runner.wait_output 'db:irb', 'ignored', expected 21 | }.to_not raise_exception 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /spec/actions/migrate_and_schema_load_and_setup_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'fileutils' 4 | 5 | RSpec.describe 'migrate and schema_load and setup actions', order: :defined do 6 | before(:all) do 7 | FileUtils.rm_f schema_location 8 | FileUtils.rm_f Dir["#{migrations_path}/*.rb"] 9 | end 10 | 11 | before{ drop_test_database_if_exists } 12 | 13 | it 'migrates and dump schema when configured to' do 14 | expect(File.exist?(schema_location)).to be false 15 | expect(rake_runner.run_task('dbtest:create dbtest:new_migration[sample] dbtest:migrate')). 16 | to be_successful 17 | expect(File.exist?(schema_location)).to be true 18 | expect(File.read schema_location).to match /INSERT INTO (public\.)?schema_migrations VALUES \('\d{14}_sample\.rb'\);/ 19 | end 20 | 21 | it 'loads from dump to a new database' do 22 | expect(rake_runner.run_task('dbtest:create')).to be_successful 23 | with_dbtest do |db| 24 | expect(db.tables).to_not include :schema_migrations 25 | expect(rake_runner.run_task('dbtest:schema_load')).to be_successful 26 | expect(db.tables).to include :schema_migrations 27 | expect(db[:schema_migrations].count).to be 1 28 | expect(db[:schema_migrations].get :filename).to match /\d{14}_sample.rb/ 29 | end 30 | end 31 | 32 | # does essentially the same as the previous example, plus load seeds if they exist 33 | it 'setup a new database from last schema dump and seeds' do 34 | File.write seeds_location, <<~SEEDS_END 35 | DB << 'create table seed(name text)' 36 | SEEDS_END 37 | expect(rake_runner.run_task('dbtest:setup')).to be_successful 38 | with_dbtest do |db| 39 | expect(db[:schema_migrations].select_map(:filename).join(';')).to match /\d{14}_sample.rb/ 40 | expect(db.tables).to include :seed 41 | end 42 | end 43 | 44 | it 'ignores the seeds part if the seeds file does not exist' do 45 | File.delete seeds_location 46 | expect(rake_runner.run_task('dbtest:setup')).to be_successful 47 | with_dbtest do |db| 48 | expect(db[:schema_migrations].select_map(:filename).join(';')).to match /\d{14}_sample.rb/ 49 | expect(db.tables).to_not include :seed 50 | end 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /spec/actions/new_migration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'fileutils' 4 | 5 | RSpec.describe 'new_migration action' do 6 | before{ FileUtils.rm_f Dir["#{migrations_path}/*.rb"] } 7 | 8 | it 'generates a new migration file' do 9 | expect(Dir["#{migrations_path}/*.rb"]).to be_empty 10 | expect(rake_runner.run_task('dbtest:new_migration[first_migration]')).to be_successful 11 | migrations = Dir["#{migrations_path}/*.rb"] 12 | expect(migrations.size).to eq 1 13 | expect(File.basename migrations[0]).to match /\A\d+_first_migration\.rb\z/ 14 | content = File.read migrations[0] 15 | expect(content).to match /# documentation available at/m 16 | expect(content).to match /\sSequel.migration do/m 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /spec/actions/reset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'fileutils' 4 | 5 | RSpec.describe 'reset action' do 6 | before do 7 | FileUtils.rm_f Dir["#{migrations_path}/*.rb"] 8 | end 9 | 10 | it 'recreates the database by rerunning all migrations in a newly created db' do 11 | File.write "#{migrations_path}/20171111111111_sample.rb", <<~MIGRATION_CONTENT 12 | Sequel.migration do 13 | change do 14 | create_table(:table_name) do 15 | primary_key :id 16 | String :name, null: false 17 | end 18 | end 19 | end 20 | MIGRATION_CONTENT 21 | File.write "#{migrations_path}/20171111111112_second.rb", <<~SECOND_MIGRATION 22 | Sequel.migration { change { create_table(:second){ primary_key :id } } } 23 | SECOND_MIGRATION 24 | File.write seeds_location, <<~SEEDS_END 25 | DB << 'create table seed(name text)' 26 | SEEDS_END 27 | rake_runner.run_task('dbtest:drop dbtest:create dbtest:migrate') 28 | with_dbtest do |db| 29 | db << 'create table a(id text)' 30 | expect(db.tables).to include :a, :table_name 31 | expect(db.tables).to_not include :seed 32 | end 33 | rake_runner.run_task('dbtest:reset') 34 | with_dbtest do |db| 35 | expect(db.tables).to include :table_name, :seed 36 | expect(db.tables).to_not include :a 37 | end 38 | expect(File.read schema_location). 39 | to match /INSERT INTO (public\.)?schema_migrations VALUES \('20171111111111_sample\.rb'\);/ 40 | expect(File.read schema_location). 41 | to match /INSERT INTO (public\.)?schema_migrations VALUES \('20171111111112_second\.rb'\);/ 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /spec/actions/schema_dump_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | require 'fileutils' 3 | 4 | RSpec.describe 'stores schema.sql' do 5 | context 'creates schema.sql on dbtest:schema_dump' do 6 | before{ FileUtils.rm_f schema_location } 7 | 8 | context 'without support for postgres adapter' do 9 | 10 | it 'fails if it cannot find support for the db adapter' do 11 | drop_test_database_if_exists 12 | expect(rake_runner.run_task('dbtest:create')).to be_successful 13 | action_result = rake_runner.run_task('-f Rakefile.no_pg_schema_dump dbtest:schema_dump') 14 | expect(action_result).to_not be_successful 15 | expect(action_result.stdout). 16 | to eq "Dumping the db schema is not currently supported for postgres. Aborting\n" 17 | expect(File.exist?(schema_location)).to be false 18 | end 19 | end 20 | 21 | it 'succeeds when no migrations have been applied' do 22 | expect(File.exist?(schema_location)).to be false 23 | drop_test_database_if_exists 24 | expect(rake_runner.run_task('dbtest:create')).to be_successful 25 | expect(rake_runner.run_task('dbtest:schema_dump')).to be_successful 26 | expect(File.exist?(schema_location)).to be true 27 | end 28 | 29 | it 'supports custom schema tables' do 30 | expect(File.exist?(schema_location)).to be false 31 | drop_test_database_if_exists 32 | expect(rake_runner.run_task('dbtest:create')).to be_successful 33 | expect(rake_runner.run_task('dbtest:migrate')).to be_successful 34 | expect(rake_runner.run_task('dbtest_schema_table:schema_dump')).to be_successful 35 | expect(File.read(schema_location)).to match /insert into (public\.)?schema_migrations values \('20171111111112_second\.rb'\);/i 36 | end 37 | 38 | # the test case exercising data from schema_migrations is exercised in the migrate specs 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/actions/seed_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | RSpec.describe 'seed action' do 4 | before(:all) do 5 | drop_test_database_if_exists 6 | rake_runner.run_task 'dbtest:create' 7 | end 8 | 9 | it 'defines the DB constant and loads seeds.rb' do 10 | File.write seeds_location, <<~SEEDS_END 11 | DB << 'create table seed(name text)' unless DB.tables.include?(:seed) 12 | DB[:seed].import [:name], [['one'], ['two']] if DB[:seed].empty? 13 | SEEDS_END 14 | with_dbtest do |db| 15 | expect(db.tables).to_not include :seed 16 | expect(rake_runner.run_task('dbtest:seed')).to be_successful 17 | expect(db.tables).to include :seed 18 | expect(db[:seed].select_order_map :name).to eq ['one', 'two'] 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /spec/actions/shell_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | RSpec.describe 'shell action' do 4 | it 'opens a sql console to the database' do 5 | expected = /---\s+sequel_tools_test\s/ 6 | expect{ 7 | rake_exec_runner.wait_output 'db:shell', 'select current_database();', expected 8 | }.to_not raise_exception 9 | end 10 | 11 | it 'allows a custom command to be provided, which will get the config through ENV' do 12 | action_result = rake_runner.run_task '-f Rakefile.custom-shell db:shell' 13 | expect(action_result).to be_successful 14 | expect(action_result.stdout).to eq "localhost-5432-sequel_tools_user-secret-sequel_tools_test\n" 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /spec/actions/up_down_redo_rollback_version_status_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'fileutils' 4 | 5 | RSpec.describe 'A complex workflow around 2 migrations being managed in different orders' do 6 | before do 7 | FileUtils.rm_f schema_location 8 | drop_test_database_if_exists 9 | end 10 | 11 | before(:all) do 12 | FileUtils.rm_f Dir["#{migrations_path}/*.rb"] 13 | File.write "#{migrations_path}/20171111111111_first.rb", <<~FIRST_MIGRATION 14 | Sequel.migration { change { create_table(:first){ primary_key :id } } } 15 | FIRST_MIGRATION 16 | File.write "#{migrations_path}/20171111111112_second.rb", <<~SECOND_MIGRATION 17 | Sequel.migration { change { create_table(:second){ primary_key :id } } } 18 | SECOND_MIGRATION 19 | end 20 | 21 | it 'runs migration up, down, redo, check version and status, migrate and rollback' do 22 | expect(File.exist?(schema_location)).to be false 23 | expect(rake_runner.run_task('dbtest:create')).to be_successful 24 | with_dbtest do |db| 25 | status_result = rake_runner.run_task('dbtest:status') 26 | expect(status_result).to be_successful 27 | expect(status_result.stdout). 28 | to eq "Unapplied migrations:\n - 20171111111111_first.rb\n - 20171111111112_second.rb\n\n" 29 | 30 | expect(db.tables).to_not include :second 31 | expect(rake_runner.run_task('dbtest:up[20171111111112_second.rb]')).to be_successful 32 | 33 | expect(rake_runner.run_task('dbtest:status').stdout). 34 | to eq "Unapplied migrations:\n - 20171111111111_first.rb\n\n" 35 | 36 | version_result = rake_runner.run_task('dbtest:version') 37 | expect(version_result).to be_successful 38 | expect(version_result.stdout).to eq "20171111111112_second.rb\n" 39 | 40 | second_attempt = rake_runner.run_task('dbtest:up[20171111111112_second]') 41 | expect(second_attempt).to_not be_successful 42 | expect(second_attempt.stdout).to eq('Expected a single unapplied migration for ' + 43 | "20171111111112_second.rb but found 0. Aborting.\n") 44 | schema = File.read(schema_location) 45 | expect(schema).to match /20171111111112_second/ 46 | expect(schema).to_not match /20171111111111_first/ 47 | expect(db.tables).to include :second 48 | 49 | expect(db[:second].count).to be 0 50 | db[:second].insert id: 1 51 | expect(db[:second].count).to be 1 52 | expect(rake_runner.run_task('dbtest:redo[20171111111112_second.rb]')).to be_successful 53 | expect(schema).to match /20171111111112_second/ 54 | expect(schema).to_not match /20171111111111_first/ 55 | expect(db.tables).to include :second 56 | expect(db[:second].count).to be 0 57 | 58 | expect(rake_runner.run_task('dbtest:down[20171111111112_second]')).to be_successful 59 | expect(db.tables).to_not include :second 60 | schema = File.read(schema_location) 61 | expect(schema).to_not match /20171111111112_second/ 62 | expect(schema).to_not match /20171111111111_first/ 63 | 64 | second_attempt = rake_runner.run_task('dbtest:down[20171111111112_second.rb]') 65 | expect(second_attempt).to_not be_successful 66 | expect(second_attempt.stdout).to eq('Expected a single unapplied migration for ' + 67 | "20171111111112_second.rb but found 0. Aborting.\n") 68 | 69 | expect(rake_runner.run_task('dbtest:migrate')).to be_successful 70 | expect(db.tables).to include :first, :second 71 | schema = File.read(schema_location) 72 | expect(schema).to match /20171111111112_second/ 73 | expect(schema).to match /20171111111111_first/ 74 | 75 | expect(rake_runner.run_task('dbtest:status').stdout). 76 | to eq "No pending migrations in the database\n" 77 | 78 | second_migration_path = "#{migrations_path}/20171111111112_second.rb" 79 | File.rename second_migration_path, "#{second_migration_path}.ignored" 80 | 81 | expect(rake_runner.run_task('dbtest:status').stdout). 82 | to eq "The following migrations were applied to the database but can't be found in " + 83 | "#{migrations_path}:\n - 20171111111112_second.rb\n\n" 84 | 85 | expect(rake_runner.run_task('dbtest:rollback')).to be_successful 86 | expect(db.tables).to_not include :first 87 | expect(db.tables).to include :second 88 | schema = File.read(schema_location) 89 | expect(schema).to_not match /20171111111111_first/ 90 | expect(schema).to match /20171111111112_second/ 91 | expect(rake_runner.run_task('dbtest:version').stdout).to eq "20171111111112_second.rb\n" 92 | 93 | expect(rake_runner.run_task('dbtest:status').stdout). 94 | to eq "The following migrations were applied to the database but can't be found in " + 95 | "#{migrations_path}:\n - 20171111111112_second.rb\n\n" + 96 | "Unapplied migrations:\n - 20171111111111_first.rb\n\n" 97 | 98 | File.rename "#{second_migration_path}.ignored", second_migration_path 99 | 100 | expect(rake_runner.run_task('dbtest:status').stdout). 101 | to eq "Unapplied migrations:\n - 20171111111111_first.rb\n\n" 102 | 103 | expect(rake_runner.run_task('dbtest:rollback')).to be_successful 104 | expect(db.tables).to_not include :first 105 | expect(db.tables).to_not include :second 106 | schema = File.read(schema_location) 107 | expect(schema).to_not match /20171111111111_first/ 108 | expect(schema).to_not match /20171111111112_second/ 109 | expect(rake_runner.run_task('dbtest:version').stdout).to eq "No migrations applied\n" 110 | 111 | expect(rake_runner.run_task('dbtest:status').stdout).to eq( 112 | "Unapplied migrations:\n - 20171111111111_first.rb\n - 20171111111112_second.rb\n\n") 113 | 114 | invalid_rollback_attempt = rake_runner.run_task('dbtest:rollback') 115 | expect(invalid_rollback_attempt).to_not be_successful 116 | expect(invalid_rollback_attempt.stdout). 117 | to eq "No existing migrations are applied - cannot rollback\n" 118 | end 119 | end 120 | end 121 | 122 | -------------------------------------------------------------------------------- /spec/fast_rake_runner.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'benchmark' 3 | require 'rake' 4 | 5 | class FastRakeRunner 6 | 7 | def self.cmd(command) 8 | if command =~ /Rakefile/ 9 | command = command.sub(/Rakefile/, "#{__dir__}/sample_project/Rakefile") 10 | else 11 | command = "-f #{__dir__}/sample_project/Rakefile #{command}" 12 | end 13 | command.split(/\s/) 14 | end 15 | 16 | def self.run_rake(args) 17 | out_reader, out_writer = IO.pipe 18 | err_reader, err_writer = IO.pipe 19 | pid = Process.fork do 20 | [out_reader, err_reader].each &:close 21 | STDOUT.reopen out_writer 22 | STDERR.reopen err_writer 23 | Rake.application.run cmd(args) 24 | end 25 | [out_writer, err_writer].each &:close 26 | pid, status = Process.wait2 pid 27 | [ out_reader.read, err_reader.read, status ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | require 'fileutils' 3 | 4 | RSpec.describe 'before hooks' do 5 | it 'support before_any, before_action, before_any_adapter and before_action_adapter' do 6 | drop_test_database_if_exists 7 | expect(rake_runner.run_task('dbtest:create')).to be_successful 8 | action_result = rake_runner.run_task('-f Rakefile.hooks dbtest:connect_db dbtest:drop') 9 | expect(action_result).to be_successful 10 | expect(action_result.stdout).to eq [ 11 | 'before_any: connect_db (processed: no)', 12 | 'before_connect_db', 13 | 'before_any_postgres', 14 | 'before_connect_db_postgres', 15 | 'before_any: drop (processed: yes)', 16 | 'before_any_postgres', 17 | '', 18 | ].join("\n") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/log_level_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'fileutils' 4 | 5 | RSpec.describe 'log_level setting' do 6 | before do 7 | FileUtils.rm_f Dir["#{migrations_path}/*.rb"] 8 | File.write "#{migrations_path}/20171111111111_first.rb", <<~FIRST_MIGRATION 9 | Sequel.migration { change { create_table(:first){ primary_key :id } } } 10 | FIRST_MIGRATION 11 | File.write "#{migrations_path}/20171111111112_second.rb", <<~SECOND_MIGRATION 12 | Sequel.migration { change { create_table(:second){ primary_key :id } } } 13 | SECOND_MIGRATION 14 | end 15 | 16 | it 'prints Sequel logs when configured to' do 17 | drop_test_database_if_exists 18 | expect(rake_runner.run_task('dbtest:create')).to be_successful 19 | reset_result = rake_runner.run_task('dbtestverbose:reset') 20 | expect(reset_result).to be_successful 21 | logs = reset_result.stdout.split "\n" 22 | logs.zip([ 23 | "[INFO] Dropped database 'sequel_tools_test_test'", 24 | "[INFO] Created database 'sequel_tools_test_test'", 25 | '[INFO] Begin applying migration 20171111111111_first.rb, direction: up', 26 | /\A\[INFO\] Finished applying migration 20171111111111_first.rb, direction: up, took 0\.\d+ seconds\z/, 27 | '[INFO] Begin applying migration 20171111111112_second.rb, direction: up', 28 | /\A\[INFO\] Finished applying migration 20171111111112_second.rb, direction: up, took 0\.\d+ seconds\z/, 29 | ]).each do |(logged, expected)| 30 | expect(logged).to (String === expected ? eq(expected) : match(expected)) 31 | end 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /spec/rake_exec_runner.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative 'rake_runner' 4 | require 'singleton' 5 | require 'pty' 6 | require 'time' 7 | 8 | class RakeExecRunner 9 | include Singleton 10 | 11 | def initialize 12 | @rake_runner = RakeRunner.instance # ensure bundle install is called once 13 | end 14 | 15 | class IncompleteResponse < StandardError; end 16 | class TimedOut < StandardError; end 17 | 18 | DEFAULT_TIMEOUT = RUBY_PLATFORM == 'java' ? 15 : 2 19 | def wait_output(task, input, regex, timeout: DEFAULT_TIMEOUT, max_length: 100000) 20 | PTY.spawn("cd #{@rake_runner.project_root} && bundle exec rake #{task}") do |r, w, pid| 21 | w.puts input 22 | output = '' 23 | wait_until = Time.now + timeout 24 | begin 25 | output += r.read_nonblock max_length 26 | raise IncompleteResponse unless output =~ regex 27 | rescue IO::WaitReadable, IncompleteResponse 28 | IO.select([r], [], [], timeout) 29 | raise TimedOut, "Output didn't match regex after #{timeout}s." if Time.now > wait_until 30 | retry 31 | end 32 | output 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/rake_runner.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'singleton' 4 | require_relative 'fast_rake_runner' 5 | 6 | class RakeRunner 7 | include Singleton 8 | 9 | attr_reader :project_root 10 | 11 | def initialize 12 | require 'open3' 13 | @project_root = File.expand_path 'sample_project', __dir__ 14 | run_cmd('bundle').success or raise "Couldn't run bundle on sample project" 15 | end 16 | 17 | def run_cmd(command) 18 | TaskResult.new "command: #{command}", *Open3.capture3(cmd(command)) 19 | end 20 | 21 | def cmd(command) 22 | %Q{cd "#{@project_root}" && #{command}} 23 | end 24 | 25 | def run_task(task) 26 | TaskResult.new task, *( 27 | ENV['FORK_RAKE'] && RUBY_PLATFORM != 'java' ? 28 | FastRakeRunner.run_rake(task) : 29 | Open3.capture3(cmd("bundle exec rake #{task}")) 30 | ) 31 | end 32 | 33 | class TaskResult 34 | attr_reader :name, :stdout, :stderr, :success, :status 35 | def initialize(name, stdout, stderr, status) 36 | @name, @stdout, @stderr, @status = name, stdout, stderr, status 37 | @success = status == 0 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/sample_project/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'sequel_tools', path: '../..' 6 | gem 'pg', platform: :mri 7 | gem 'jdbc-postgres', platform: :jruby 8 | gem 'rake' 9 | -------------------------------------------------------------------------------- /spec/sample_project/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | sequel_tools (0.1.13) 5 | sequel 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | jdbc-postgres (42.2.14) 11 | pg (1.2.3) 12 | rake (13.0.3) 13 | sequel (5.61.0) 14 | 15 | PLATFORMS 16 | java 17 | ruby 18 | universal-java-1.8 19 | 20 | DEPENDENCIES 21 | jdbc-postgres 22 | pg 23 | rake 24 | sequel_tools! 25 | 26 | BUNDLED WITH 27 | 2.2.17 28 | -------------------------------------------------------------------------------- /spec/sample_project/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel_tools' 4 | 5 | base_config = { dbadapter: 'postgres', dbname: 'sequel_tools_test', 6 | username: 'sequel_tools_user' } 7 | 8 | namespace 'db' do 9 | SequelTools.inject_rake_tasks base_config, self 10 | end 11 | 12 | namespace 'dbpw' do 13 | SequelTools.inject_rake_tasks base_config.merge(dbname: 'sequel_tools_test_pw', 14 | password: 'secret'), self 15 | end 16 | 17 | dbtest_config = base_config.merge(dbname: 'sequel_tools_test_test', 18 | maintenancedb: 'sequel_tools_test', 19 | dump_schema_on_migrate: true) 20 | namespace 'dbtest' do 21 | SequelTools.inject_rake_tasks dbtest_config, self 22 | end 23 | 24 | namespace 'dbtestverbose' do 25 | SequelTools.inject_rake_tasks dbtest_config.merge(log_level: :info), self 26 | end 27 | 28 | namespace 'dbtest_schema_table' do 29 | SequelTools.inject_rake_tasks dbtest_config.merge(schema_table: :custom_schema_mig), self 30 | end 31 | -------------------------------------------------------------------------------- /spec/sample_project/Rakefile.custom-shell: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel_tools' 4 | 5 | config = { dbadapter: 'postgres', dbname: 'sequel_tools_test', 6 | username: 'sequel_tools_user', dbhost: 'localhost', dbport: 5432, password: 'secret', 7 | shell_command: File.expand_path('custom-shell', __dir__)} 8 | 9 | namespace 'db' do 10 | SequelTools.inject_rake_tasks config, self 11 | end 12 | -------------------------------------------------------------------------------- /spec/sample_project/Rakefile.hooks: -------------------------------------------------------------------------------- 1 | require 'sequel_tools/actions_manager' 2 | require 'sequel_tools/all_actions' 3 | 4 | register = ->(*args, &block){ SequelTools::ActionsManager::Action.register *args, &block } 5 | 6 | register.call(:before_any, nil) do |args, context| 7 | processed = context[:before_any_processed] ? 'yes' : 'no' 8 | puts "before_any: #{context[:current_action].name} (processed: #{processed})" 9 | end 10 | 11 | register.call(:before_connect_db, nil){|args, context| puts 'before_connect_db' } 12 | 13 | old_action = SequelTools::ActionsManager::Action.unregister :before_any_postgres 14 | register.call(:before_any_postgres, nil) do |args, context| 15 | old_action.run args, context 16 | puts 'before_any_postgres' 17 | end 18 | 19 | register.call(:before_connect_db_postgres, nil) do |args, context| 20 | puts 'before_connect_db_postgres' 21 | end 22 | 23 | load File.join __dir__, 'Rakefile' 24 | -------------------------------------------------------------------------------- /spec/sample_project/Rakefile.no_pg_schema_dump: -------------------------------------------------------------------------------- 1 | require 'sequel_tools/actions_manager' 2 | require 'sequel_tools/all_actions' 3 | SequelTools::ActionsManager::Action.unregister :schema_dump_postgres 4 | load File.join __dir__, 'Rakefile' 5 | -------------------------------------------------------------------------------- /spec/sample_project/custom-shell: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo $DBHOST-$DBPORT-$DBUSERNAME-$DBPASSWORD-$DBNAME 3 | -------------------------------------------------------------------------------- /spec/sequel_tools_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'sequel' 4 | require 'sequel_tools' 5 | 6 | RSpec.describe SequelTools do 7 | it 'has a version number' do 8 | expect(SequelTools::VERSION).not_to be nil 9 | end 10 | 11 | context 'Base configuration' do 12 | it 'raises if a required key is missing' do 13 | expect{ SequelTools.base_config }.to raise_error SequelTools::MissingConfigError 14 | end 15 | 16 | it 'returns a basic configuration given the minimum required information' do 17 | config = SequelTools.base_config project_root: '/project_root', dbadapter: 'postgres', 18 | dbname: 'mydb', username: 'myuser' 19 | expect(config[:migrations_location]).to eq '/project_root/db/migrations' 20 | expect(config[:schema_location]).to eq '/project_root/db/migrations/schema.sql' 21 | expect(config[:seeds_location]).to eq '/project_root/db/seeds.rb' 22 | end 23 | end 24 | 25 | context 'Test database and user exist' do 26 | def connect(uri) 27 | uri = (RUBY_PLATFORM == 'java' ? 'jdbc:postgresql://' : 'postgres://') + uri 28 | Sequel.connect uri 29 | end 30 | 31 | it 'can connect to sequel_tools_test without using a password' do 32 | db = connect 'localhost/sequel_tools_test?user=sequel_tools_user' 33 | expect(db['select 1'].get).to eq 1 34 | end 35 | 36 | it 'can connect to sequel_tools_test_pw using a password' do 37 | db = connect 'localhost/sequel_tools_test_pw?user=sequel_tools_user&password=secret' 38 | expect(db['select 1'].get).to eq 1 39 | end 40 | 41 | it 'cannot connect to sequel_tools_test_pw without a password' do 42 | expect{ 43 | SequelTools.suppress_java_output do 44 | connect 'localhost/sequel_tools_test_pw?user=sequel_tools_user' 45 | end 46 | }.to raise_error Sequel::DatabaseConnectionError 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | # just in case, as it seems it may cause some weird behavior with JRuby: 6 | ['PGDATABASE', 'PGHOST', 'PGUSER', 'PGPORT', 'PGPASSWORD'].each{|e| ENV.delete e } 7 | 8 | module SpecHelpers 9 | CACHE = {} 10 | 11 | def sample_project_root 12 | CACHE[:sample_project_root] ||= File.expand_path 'sample_project', __dir__ 13 | end 14 | 15 | def schema_location 16 | CACHE[:schema_location] ||= File.join migrations_path, 'schema.sql' 17 | end 18 | 19 | def migrations_path 20 | CACHE[:migrations_path] ||= File.join sample_project_root, 'db/migrations' 21 | end 22 | 23 | def seeds_location 24 | CACHE[:seeds_location] ||= File.join sample_project_root, 'db/seeds.rb' 25 | end 26 | 27 | def db 28 | return CACHE[:db] if CACHE[:db] 29 | require 'sequel' 30 | CACHE[:db] = make_connection 'localhost/sequel_tools_test?user=sequel_tools_user' 31 | end 32 | 33 | def make_connection(uri) 34 | if RUBY_PLATFORM == 'java' 35 | uri = "jdbc:postgresql://#{uri}" 36 | else 37 | uri = "postgres://#{uri}" 38 | end 39 | result = Sequel.connect uri 40 | if ENV['FORK_RAKE'] && RUBY_PLATFORM != 'java' 41 | result.extension :connection_validator 42 | result.pool.connection_validation_timeout = -1 43 | end 44 | result 45 | end 46 | 47 | def with_dbtest 48 | require 'sequel' 49 | dbtest = make_connection 'localhost/sequel_tools_test_test?user=sequel_tools_user' 50 | yield dbtest 51 | dbtest.disconnect 52 | end 53 | 54 | def drop_test_database_if_exists 55 | db << 'drop database if exists sequel_tools_test_test' 56 | end 57 | 58 | def rake_runner 59 | require_relative 'rake_runner' 60 | RakeRunner.instance 61 | end 62 | 63 | def rake_exec_runner 64 | require_relative 'rake_exec_runner' 65 | RakeExecRunner.instance 66 | end 67 | end 68 | 69 | RSpec.configure do |config| 70 | # Enable flags like --only-failures and --next-failure 71 | config.example_status_persistence_file_path = '.rspec_status' 72 | 73 | # Disable RSpec exposing methods globally on `Module` and `main` 74 | config.disable_monkey_patching! 75 | 76 | config.expect_with :rspec do |c| 77 | c.syntax = :expect 78 | end 79 | 80 | config.include SpecHelpers 81 | config.before(:suite) do 82 | require 'fileutils' 83 | FileUtils.mkdir_p File.join(__dir__, 'sample_project/db/migrations') 84 | end 85 | end 86 | 87 | require 'rspec/expectations' 88 | 89 | RSpec::Matchers.define :be_successful do 90 | match do |action_result| 91 | action_result.success == true && action_result.stderr.empty? 92 | end 93 | 94 | failure_message do |action_result| 95 | msg = [ "Task '#{action_result.name}' failed with status '#{action_result.status}'" ] 96 | msg << "\nstdout:\n" << action_result.stdout unless action_result.stdout.empty? 97 | msg << "\nstderr:\n" << action_result.stderr unless action_result.stderr.empty? 98 | msg.join "\n" 99 | end 100 | end 101 | --------------------------------------------------------------------------------