diff --git a/scripts/regenerate-schema b/scripts/regenerate-schema
new file mode 100755
index 0000000000000000000000000000000000000000..b63a75cdc835c3766ea268f20f38404e2a2164da
--- /dev/null
+++ b/scripts/regenerate-schema
@@ -0,0 +1,194 @@
+#!/usr/bin/env ruby
+
+# frozen_string_literal: true
+
+require 'net/http'
+require 'uri'
+
+class SchemaRegenerator
+  ##
+  # Filename of the schema
+  #
+  # This file is being regenerated by this script.
+  FILENAME = 'db/structure.sql'
+
+  ##
+  # Directories where migrations are stored
+  #
+  # The methods +hide_migrations+ and +unhide_migrations+ will rename
+  # these to disable/enable migrations.
+  MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
+
+  def execute
+    Dir.chdir(File.expand_path('..', __dir__)) do
+      checkout_ref
+      checkout_clean_schema
+      hide_migrations
+      reset_db
+      unhide_migrations
+      migrate
+    ensure
+      unhide_migrations
+    end
+  end
+
+  private
+
+  ##
+  # Git checkout +CI_COMMIT_SHA+.
+  #
+  # When running from CI, checkout the clean commit,
+  # not the merged result.
+  def checkout_ref
+    return unless ci?
+
+    run %Q[git checkout #{source_ref}]
+    run %q[git clean -f -- db]
+  end
+
+  ##
+  # Checkout the clean schema from the target branch
+  def checkout_clean_schema
+    remote_checkout_clean_schema || local_checkout_clean_schema
+  end
+
+  ##
+  # Get clean schema from remote servers
+  #
+  # This script might run in CI, using a shallow clone, so to checkout
+  # the file, download it from the server.
+  def remote_checkout_clean_schema
+    return false unless project_url
+
+    uri = URI.join("#{project_url}/", 'raw/', "#{merge_base}/", FILENAME)
+
+    download_schema(uri)
+  end
+
+  ##
+  # Download the schema from the given +uri+.
+  def download_schema(uri)
+    puts "Downloading #{uri}..."
+
+    Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+      request = Net::HTTP::Get.new(uri.request_uri)
+      http.read_timeout = 500
+      http.request(request) do |response|
+        raise("Failed to download file: #{response.code} #{response.message}") if response.code.to_i != 200
+
+        File.open(FILENAME, 'w') do |io|
+          response.read_body do |chunk|
+            io.write(chunk)
+          end
+        end
+      end
+    end
+
+    true
+  end
+
+  ##
+  # Git checkout the schema from target branch.
+  #
+  # Ask git to checkout the schema from the target branch and reset
+  # the file to unstage the changes.
+  def local_checkout_clean_schema
+    run %Q[git checkout #{merge_base} -- #{FILENAME}]
+    run %Q[git reset -- #{FILENAME}]
+  end
+
+  ##
+  # Move migrations to where Rails will not find them.
+  #
+  # To reset the database to clean schema defined in +FILENAME+, move
+  # the migrations to a path where Rails will not find them, otherwise
+  # +db:reset+ would abort. Later when the migrations should be
+  # applied, use +unhide_migrations+ to bring them back.
+  def hide_migrations
+    MIGRATION_DIRS.each do |dir|
+      File.rename(dir, "#{dir}__")
+    end
+  end
+
+  ##
+  # Undo the effect of +hide_migrations+.
+  #
+  # Place back the migrations which might be moved by
+  # +hide_migrations+.
+  def unhide_migrations
+    error = nil
+
+    MIGRATION_DIRS.each do |dir|
+      File.rename("#{dir}__", dir)
+    rescue Errno::ENOENT
+      nil
+    rescue StandardError => e
+      # Save error for later, but continue with other dirs first
+      error = e
+    end
+
+    raise error if error
+  end
+
+  ##
+  # Run rake task to reset the database.
+  def reset_db
+    run %q[bin/rails db:reset RAILS_ENV=test]
+  end
+
+  ##
+  # Run rake task to run migrations.
+  def migrate
+    run %q[bin/rails db:migrate RAILS_ENV=test]
+  end
+
+  ##
+  # Run the given +cmd+.
+  #
+  # The command is colored green, and the output of the command is
+  # colored gray.
+  # When the command failed an exception is raised.
+  def run(cmd)
+    puts "\e[32m$ #{cmd}\e[37m"
+    ret = system(cmd)
+    puts "\e[0m"
+    raise("Command failed") unless ret
+  end
+
+  ##
+  # Return the base commit between source and target branch.
+  def merge_base
+    @merge_base ||= `git merge-base #{target_branch} #{source_ref}`.chomp
+  end
+
+  ##
+  # Return the name of the target branch
+  #
+  # Get source ref from CI environment variable, or read the +TARGET+
+  # environment+ variable, or default to +HEAD+.
+  def target_branch
+    ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || 'master'
+  end
+
+  ##
+  # Return the source ref
+  #
+  # Get source ref from CI environment variable, or default to +HEAD+.
+  def source_ref
+    ENV['CI_COMMIT_SHA'] || 'HEAD'
+  end
+
+  ##
+  # Return the project URL from CI environment variable.
+  def project_url
+    ENV['CI_PROJECT_URL']
+  end
+
+  ##
+  # Return whether the script is running from CI
+  def ci?
+    ENV['CI']
+  end
+end
+
+SchemaRegenerator.new.execute