diff --git a/config/database.cr b/config/database.cr index 393c066a8..710249982 100644 --- a/config/database.cr +++ b/config/database.cr @@ -15,6 +15,18 @@ TestDatabase.configure do |settings| ) end +class SampleBackupDatabase < Avram::Database +end + +SampleBackupDatabase.configure do |settings| + settings.credentials = Avram::Credentials.parse?(ENV["BACKUP_DATABASE_URL"]?) || Avram::Credentials.new( + hostname: "db", + database: "sample_backup", + username: "lucky", + password: "developer" + ) +end + DatabaseWithIncorrectSettings.configure do |settings| settings.credentials = Avram::Credentials.new( hostname: "db", diff --git a/spec/avram/migrator/runner_spec.cr b/spec/avram/migrator/runner_spec.cr index 51b660a7a..0627f7106 100644 --- a/spec/avram/migrator/runner_spec.cr +++ b/spec/avram/migrator/runner_spec.cr @@ -11,4 +11,37 @@ describe Avram::Migrator::Runner do end end end + + describe ".create_db" do + context "when the DB doesn't exist yet" do + it "creates the new DB" do + Avram.temp_config(database_to_migrate: SampleBackupDatabase) do + Avram::Migrator::Runner.create_db(quiet?: true) + + DB.open(SampleBackupDatabase.credentials.url) do |db| + results = db.query_all("SELECT datname FROM pg_database WHERE datistemplate = false", as: String) + results.should contain("sample_backup") + end + # ensure it's deleted before moving on to another spec + Avram::Migrator::Runner.drop_db(quiet?: true) + end + end + end + end + + describe ".drop_db" do + context "when it already exists" do + it "drops the DB" do + Avram.temp_config(database_to_migrate: SampleBackupDatabase) do + Avram::Migrator::Runner.create_db(quiet?: true) + Avram::Migrator::Runner.drop_db(quiet?: true) + + expect_raises(DB::ConnectionRefused) do + DB.open(SampleBackupDatabase.credentials.url) do |_| + end + end + end + end + end + end end diff --git a/spec/avram/tasks/db_create_spec.cr b/spec/avram/tasks/db_create_spec.cr index 3995cbc9d..77d6fe1b1 100644 --- a/spec/avram/tasks/db_create_spec.cr +++ b/spec/avram/tasks/db_create_spec.cr @@ -1,11 +1,9 @@ require "../../spec_helper" describe Db::Create do - # This is currently failing with the wrong message. - # it may be related to https://github.com/actions/virtual-environments/issues/4269 - pending "raises a connection error when unable to connect" do + it "raises a connection error when unable to connect" do Avram.temp_config(database_to_migrate: DatabaseWithIncorrectSettings) do - expect_raises(Exception, /It looks like Postgres is not running/) do + expect_raises(Avram::ConnectionError, /Failed to connect to database/) do Db::Create.new(quiet: true).run_task end end diff --git a/spec/avram/tasks/db_schema_restore_spec.cr b/spec/avram/tasks/db_schema_restore_spec.cr index cfd7c9217..8501c591b 100644 --- a/spec/avram/tasks/db_schema_restore_spec.cr +++ b/spec/avram/tasks/db_schema_restore_spec.cr @@ -16,10 +16,7 @@ describe Db::Schema::Restore do end it "restores from the sample_backup file" do - Avram.temp_config(database_to_migrate: SampleBackupDatabase) do - Db::Drop.new(quiet: true).run_task - Db::Create.new(quiet: true).run_task - + swap_database_with_cleanup(SampleBackupDatabase) do Db::Schema::Restore.new(SQL_DUMP_FILE).run_task SampleBackupDatabase.run do |db| @@ -29,3 +26,17 @@ describe Db::Schema::Restore do end end end + +private def swap_database_with_cleanup(database_to_migrate, &) + Avram.temp_config(database_to_migrate: database_to_migrate) do + # Create this new DB + Avram::Migrator::Runner.create_db(quiet?: true) + + yield + + # Ensure all connections to this DB are closed + SampleBackupDatabase.close_connections! + # make sure this is dropped before another spec runs + Avram::Migrator::Runner.drop_db(quiet?: true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 8d2dd6d33..5b9f844c0 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -34,18 +34,6 @@ Spec.before_each do Fiber.current.query_cache = LuckyCache::NullStore.new end -class SampleBackupDatabase < Avram::Database -end - -SampleBackupDatabase.configure do |settings| - settings.credentials = Avram::Credentials.parse?(ENV["BACKUP_DATABASE_URL"]?) || Avram::Credentials.new( - hostname: "db", - database: "sample_backup", - username: "lucky", - password: "developer" - ) -end - Lucky::Session.configure do |settings| settings.key = "_app_session" end diff --git a/src/avram/connection.cr b/src/avram/connection.cr index cb6c8bf9f..418e73e0b 100644 --- a/src/avram/connection.cr +++ b/src/avram/connection.cr @@ -1,10 +1,17 @@ # Handles the connection to the DB. class Avram::Connection + private getter db : DB::Database? = nil + def initialize(@connection_string : String, @database_class : Avram::Database.class) end def open : DB::Database - try_connection! + @db = try_connection! + end + + def close + @db.try(&.close) + @db = nil end def connect_listen(*channels : String, &block : PQ::Notification ->) : Nil diff --git a/src/avram/credentials.cr b/src/avram/credentials.cr index 35ba1ce8a..ec42a5722 100644 --- a/src/avram/credentials.cr +++ b/src/avram/credentials.cr @@ -80,6 +80,17 @@ class Avram::Credentials @query.try(&.strip).presence end + # This is the full connection string used + # to connect to the PostgreSQL server. + def connection_string : String + String.build do |io| + set_url_protocol(io) + set_url_creds(io) + set_url_host(io) + set_url_port(io) + end + end + # Returns the postgres connection string without # any query params. def url_without_query_params : String @@ -88,10 +99,7 @@ class Avram::Credentials private def build_url String.build do |io| - set_url_protocol(io) - set_url_creds(io) - set_url_host(io) - set_url_port(io) + io << connection_string set_url_db(io) set_url_query(io) end diff --git a/src/avram/database.cr b/src/avram/database.cr index 8612af05d..ebeef465a 100644 --- a/src/avram/database.cr +++ b/src/avram/database.cr @@ -158,6 +158,12 @@ abstract class Avram::Database end end + # Close all available connections as well as the DB + def self.close_connections! + connections.values.map(&.close) + @@db.try(&.close) + end + # :nodoc: def listen(*channels : String, &block : PQ::Notification ->) : Nil connection.connect_listen(*channels, &block) diff --git a/src/avram/migrator/runner.cr b/src/avram/migrator/runner.cr index 768c4156a..bdd34d976 100644 --- a/src/avram/migrator/runner.cr +++ b/src/avram/migrator/runner.cr @@ -48,33 +48,35 @@ class Avram::Migrator::Runner end def self.drop_db(quiet? : Bool = false) - run "dropdb #{cmd_args}" + DB.connect("#{credentials.connection_string}/#{db_user}") do |db| + db.exec "DROP DATABASE IF EXISTS #{db_name}" + end unless quiet? puts "Done dropping #{Avram::Migrator::Runner.db_name.colorize(:green)}" end - rescue e : Exception - if (message = e.message) && message.includes?(%("#{self.db_name}" does not exist)) - unless quiet? - puts "Already dropped #{self.db_name.colorize(:green)}" - end - else - raise e - end end def self.create_db(quiet? : Bool = false) - run "createdb #{cmd_args}" + DB.connect("#{credentials.connection_string}/#{db_user}") do |db| + db.exec "CREATE DATABASE #{db_name} WITH OWNER DEFAULT" + end unless quiet? - puts "Done creating #{Avram::Migrator::Runner.db_name.colorize(:green)}" + puts "Done creating #{db_name.colorize(:green)}" + end + rescue e : DB::ConnectionRefused + message = e.message.to_s + if message.blank? + raise ConnectionError.new(URI.parse(credentials.url_without_query_params), Avram.settings.database_to_migrate) + else + raise e end rescue e : Exception - if (message = e.message) && message.includes?(%("#{self.db_name}" already exists)) + message = e.message.to_s + if message.includes?(%("#{self.db_name}" already exists)) unless quiet? puts "Already created #{self.db_name.colorize(:green)}" end - elsif (message = e.message) && (message.includes?("createdb: not found") || message.includes?("No command 'createdb' found")) - raise PGClientNotInstalledError.new(message) - elsif (message = e.message) && message.includes?("could not connect to database template") + elsif message.includes?("Cannot establish connection") raise PGNotRunningError.new(message) else raise e