diff --git a/README.md b/README.md index 64a917ad..2d90f030 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable. +When using PostgreSQL providers that offer a pooled connection string for application traffic and a direct connection string for schema changes, keep `DATABASE_URL` set to the application connection and set `MIGRATION_DATABASE_URL` to the direct connection string. This lets DocuSeal run startup migrations, such as adding `NOT NULL` constraints, outside transaction poolers that do not support all migration operations. + #### Docker Compose Download docker-compose.yml into your private server: diff --git a/config/initializers/migrate.rb b/config/initializers/migrate.rb index 1e0704ed..1e4376ea 100644 --- a/config/initializers/migrate.rb +++ b/config/initializers/migrate.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require Rails.root.join('lib/migration_database_url') + Rails.configuration.to_prepare do - ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production' && ENV['RUN_MIGRATIONS'] != 'false' + MigrationDatabaseUrl.migrate if ENV['RAILS_ENV'] == 'production' && ENV['RUN_MIGRATIONS'] != 'false' end diff --git a/lib/migration_database_url.rb b/lib/migration_database_url.rb new file mode 100644 index 00000000..6ed30530 --- /dev/null +++ b/lib/migration_database_url.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module MigrationDatabaseUrl + module_function + + def migrate + migration_database_url = ENV.fetch('MIGRATION_DATABASE_URL', '').to_s + + return ActiveRecord::Tasks::DatabaseTasks.migrate if migration_database_url.empty? + + app_database_config = ActiveRecord::Base.connection_db_config + + ActiveRecord::Base.establish_connection(migration_database_url) + ActiveRecord::Tasks::DatabaseTasks.migrate + ensure + ActiveRecord::Base.establish_connection(app_database_config) if app_database_config + end +end diff --git a/spec/lib/migration_database_url_spec.rb b/spec/lib/migration_database_url_spec.rb new file mode 100644 index 00000000..96bc91cb --- /dev/null +++ b/spec/lib/migration_database_url_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' +require Rails.root.join('lib/migration_database_url') + +RSpec.describe MigrationDatabaseUrl do + around do |example| + original_env = ENV.to_hash + + example.run + ensure + ENV.replace(original_env) + end + + describe '.migrate' do + it 'runs migrations with the application database when no migration URL is configured' do + ENV.delete('MIGRATION_DATABASE_URL') + + expect(ActiveRecord::Base).not_to receive(:establish_connection) + expect(ActiveRecord::Tasks::DatabaseTasks).to receive(:migrate) + + described_class.migrate + end + + it 'uses MIGRATION_DATABASE_URL for migrations and restores the application database' do + original_config = Object.new + migration_database_url = 'postgres://user:***@ep-direct.us-east-1.aws.neon.tech/docuseal?sslmode=require' + connections = [] + + ENV['MIGRATION_DATABASE_URL'] = migration_database_url + + allow(ActiveRecord::Base).to receive(:connection_db_config).and_return(original_config) + allow(ActiveRecord::Base).to receive(:establish_connection) { |config| connections << config } + + expect(ActiveRecord::Tasks::DatabaseTasks).to receive(:migrate) + + described_class.migrate + + expect(connections).to eq([migration_database_url, original_config]) + end + + it 'restores the application database when migration fails' do + original_config = Object.new + migration_database_url = 'postgres://user:***@ep-direct.us-east-1.aws.neon.tech/docuseal?sslmode=require' + connections = [] + + ENV['MIGRATION_DATABASE_URL'] = migration_database_url + + allow(ActiveRecord::Base).to receive(:connection_db_config).and_return(original_config) + allow(ActiveRecord::Base).to receive(:establish_connection) { |config| connections << config } + allow(ActiveRecord::Tasks::DatabaseTasks).to receive(:migrate).and_raise(ActiveRecord::StatementInvalid) + + expect { described_class.migrate }.to raise_error(ActiveRecord::StatementInvalid) + expect(connections).to eq([migration_database_url, original_config]) + end + end +end