diff --git a/lib/open_project/database.rb b/lib/open_project/database.rb index 62ee9c4a7b..5be2769599 100644 --- a/lib/open_project/database.rb +++ b/lib/open_project/database.rb @@ -34,6 +34,8 @@ module OpenProject # syntax differences. module Database + class InsufficientVersionError < StandardError; end + # This method returns a hash which maps the identifier of the supported # adapter to a regex matching the adapter_name. def self.supported_adapters @@ -43,6 +45,53 @@ module OpenProject }) end + ## + # Get the database system requirements + def self.required_versions + { + postgresql: { + numeric: 90500, # PG_VERSION_NUM + string: '9.5.0', + enforced: true + }, + mysql: { + string: '5.6.0', + enforced: false + } + } + end + + ## + # Check the database version compatibility. + # Raises an +InsufficientVersionError+ when the version is incompatible + def self.check_version! + required = required_versions[name] + current = version + + return if version_matches? + message = "Database server version mismatch: Required version is #{required[:string]}, " \ + "but current version is #{current}" + + if required[:enforced] + raise InsufficientVersionError.new message + else + warn "#{message}. Version is not enforced for this database however, so continuing with this version." + end + end + + ## + # Return +true+ if the required version is matched by the current connection. + def self.version_matches? + required = required_versions[name] + + case name + when :mysql + Gem::Version.new(version) >= Gem::Version.new(required[:string]) + when :postgresql + numeric_version >= required[:numeric] + end + end + # Get the raw name of the currently used database adapter. # This string is set by the used adapter gem. def self.adapter_name(connection) @@ -75,11 +124,20 @@ module OpenProject def self.version(raw = false) case name when :mysql - version = ActiveRecord::Base.connection.select_value('SELECT VERSION()') + ActiveRecord::Base.connection.select_value('SELECT VERSION()') when :postgresql version = ActiveRecord::Base.connection.select_value('SELECT version()') raw ? version : version.match(/\APostgreSQL (\S+)/i)[1] end end + + def self.numeric_version + case name + when :mysql + raise ArgumentError, "Can't get numeric version of MySQL" + when :postgresql + ActiveRecord::Base.connection.select_value('SHOW server_version_num;').to_i + end + end end end diff --git a/lib/tasks/database.rake b/lib/tasks/database.rake index 5cf398fed3..246738d9ce 100644 --- a/lib/tasks/database.rake +++ b/lib/tasks/database.rake @@ -37,3 +37,37 @@ namespace 'db:sessions' do ActiveRecord::Base.connection.execute "DELETE FROM #{sessions_table} WHERE updated_at < '#{expiration_time}'" end end + +namespace 'openproject' do + namespace 'db' do + desc 'Expire old sessions from the sessions table' + task ensure_database_compatibility: [:environment, 'db:load_config'] do + + override = ActiveModel::Type::Boolean.new.cast ENV['OPENPROJECT_SKIP_DATABASE_VERSION_CHECK'] + if override + warn "Skipping database version check as 'OPENPROJECT_SKIP_DATABASE_VERSION_CHECK' is set. " \ + "Incompatibilites and errors may occur." + next + end + + ## + # Ensure database server version is compatible + override_msg = "If you're sure you want to skip the check, set 'OPENPROJECT_SKIP_DATABASE_VERSION_CHECK' to override" + begin + OpenProject::Database::check_version! + rescue OpenProject::Database::InsufficientVersionError => e + warn "#{e.message}. #{override_msg}" + Kernel.exit(1) + rescue ActiveRecord::ActiveRecordError => e + warn "Failed to perform postgres version check: #{e} - #{e.message}. #{override_msg}" + raise e + end + end + end +end + + +Rake::Task["db:migrate"].enhance do + Rake::Task["openproject:db:ensure_database_compatibility"].invoke +end +