Browse Source

Fixes #4431 - Missing rake task to fix json values migrated from maria/mysql to postgres.

Dusan Vuckovic 2 years ago
parent
commit
5f2ad0f497

+ 16 - 0
.gitlab/check_database_migration_consistency.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+
+set -e
+
+TMP_FILE_BEFORE='./tmp/before-migration-dump.json'
+TMP_FILE_AFTER='./tmp/after-migration-dump.json'
+
+echo "Checking if data is still the same after migration..."
+if ! cmp $TMP_FILE_BEFORE $TMP_FILE_AFTER
+then
+  echo "Data mismatch after migration."
+  diff $TMP_FILE_BEFORE $TMP_FILE_AFTER
+  exit 1
+else
+  echo "Migration was successful."
+fi

+ 51 - 0
.gitlab/check_postgres_array_columns.rb

@@ -0,0 +1,51 @@
+#!/usr/bin/env ruby
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require ::File.expand_path('../config/environment', __dir__)
+
+class CheckPostgresArrayColumns
+  def self.run
+    if Rails.configuration.database_configuration[Rails.env]['adapter'] != 'postgresql'
+      puts 'Error: This script works only with postgresql adapter!'
+      exit 1
+    end
+
+    puts 'Checking data type of array columns:'
+
+    check_columns
+
+    puts 'done.'
+  end
+
+  def self.check_columns
+    public_links.concat(object_manager_attributes).each do |item|
+      print "  #{item[:table]}.#{item[:column]} ... "
+
+      type = data_type(item[:table], item[:column])
+
+      if type == 'ARRAY'
+        puts 'OK'
+      else
+        puts 'Not OK!'
+        puts "    Expected type ARRAY, but got: #{type}"
+        exit 1
+      end
+    end
+  end
+
+  def self.object_manager_attributes
+    ObjectManager::Attribute.where(data_type: %w[multiselect multi_tree_select]).map do |field|
+      { table: field.object_lookup.name.constantize.table_name, column: field.name }
+    end
+  end
+
+  def self.public_links
+    [{ table: PublicLink.table_name, column: 'screen' }]
+  end
+
+  def self.data_type(table, column)
+    ActiveRecord::Base.connection.execute("select data_type from information_schema.columns where table_name = '#{table}' and column_name = '#{column}' limit 1")[0]['data_type']
+  end
+end
+
+CheckPostgresArrayColumns.run

+ 46 - 0
.gitlab/ci/test/migration-from-mysql-to-postgresql.yml

@@ -0,0 +1,46 @@
+#
+# Test the migration from mysql/mariadb to postgresql.
+#
+.template_migration_from_mysql_to_postgresql:
+  stage: test
+  rules:
+    - if: $CI_MERGE_REQUEST_ID
+      when: never
+    - if: '$CI_COMMIT_BRANCH =~ /^private/'
+      when: manual
+      allow_failure: true
+    - when: on_success
+  variables:
+    # Turn off not not require redis for MySQL/MariaDB.
+    ENABLE_EXPERIMENTAL_MOBILE_FRONTEND: ''
+    ENFORCE_DB_SERVICE: mysql
+  script:
+    - !reference [.scripts, configure_environment]
+    - !reference [.scripts, zammad_db_init]
+    - 'bundle exec rails r "FillDb.load(object_manager_attributes: {user: {multiselect: 1, multi_tree_select: 1}, ticket: {multiselect: 1, multi_tree_select: 1}, organization: {multiselect: 1, multi_tree_select: 1}}, organization: 2, agents: 2, tickets: 10, public_links: 2, nice: 0)"'
+    - bundle exec rake zammad:db:pgloader > tmp/pgloader-command
+    - sed -i 's#pgsql://zammad:pgsql_password@localhost/zammad#pgsql://zammad:zammad@postgresql/zammad_test#' tmp/pgloader-command
+    - cat tmp/pgloader-command # for debugging
+    - bundle exec rails r 'pp Ticket.all.as_json; pp User.all.as_json; pp Organization.all.as_json; pp PublicLink.all.as_json' > tmp/before-migration-dump.json
+    - rm -f config/database.yml && export ENFORCE_DB_SERVICE=postgresql
+    - !reference [.scripts, configure_environment]
+    - bundle exec rake db:drop db:create # re-create an empty database
+    - pgloader --verbose tmp/pgloader-command
+    - bundle exec rails r 'Rails.cache.clear'
+    - bundle exec rails r 'pp Ticket.all.as_json; pp User.all.as_json; pp Organization.all.as_json; pp PublicLink.all.as_json' > tmp/after-migration-dump.json
+    - .gitlab/check_database_migration_consistency.sh
+    - .gitlab/check_postgres_array_columns.rb
+
+migration:database:mysql_to_postgresql:
+  extends:
+    - .template_migration_from_mysql_to_postgresql
+  services:
+    - !reference [.services, mysql]
+    - !reference [.services, postgresql]
+
+migration:database:mariadb_to_postgresql:
+  extends:
+    - .template_migration_from_mysql_to_postgresql
+  services:
+    - !reference [.services, mariadb]
+    - !reference [.services, postgresql]

+ 1 - 0
.rubocop/default.yml

@@ -429,6 +429,7 @@ Zammad/DetectTranslatableString:
     - "lib/sequencer/**/*.rb"
     - "lib/import/**/*.rb"
     - "lib/tasks/**/*.rb"
+    - "lib/fill_db.rb"
 
 Zammad/ForbidTranslatableMarker:
   Enabled: true

+ 408 - 17
lib/fill_db.rb

@@ -8,6 +8,16 @@ module FillDb
 fill your database with demo records
 
   FillDb.load(
+    object_manager_attributes: {
+      user: {
+        'input': 1,
+        'multiselect': 1,
+      },
+      ticket: {
+        'textarea': 1,
+        'multiselect': 1,
+      },
+    },
     agents: 50,
     customers: 1000,
     groups: 20,
@@ -16,6 +26,7 @@ fill your database with demo records
     tickets: 100,
     knowledge_base_answers: 100,
     knowledge_base_categories: 20,
+    public_links: 2,
     nice: 0,
   )
 
@@ -28,6 +39,7 @@ or if you only want to create 100 tickets
   FillDb.load(tickets: 10000, nice: 0)
   FillDb.load(knowledge_base_answers: 100, nice: 0)
   FillDb.load(knowledge_base_categories: 20, nice: 0)
+  FillDb.load(public_links: 2, nice: 0)
 
 =end
 
@@ -39,7 +51,9 @@ or if you only want to create 100 tickets
   end
 
   def self.load_data(params)
-    nice                      = params[:nice] || 0.5
+    nice = params[:nice] || 0.5
+
+    object_manager_attributes = params[:object_manager_attributes]
     agents                    = params[:agents] || 0
     customers                 = params[:customers] || 0
     groups                    = params[:groups] || 0
@@ -48,8 +62,10 @@ or if you only want to create 100 tickets
     tickets                   = params[:tickets] || 0
     knowledge_base_answers    = params[:knowledge_base_answers] || 0
     knowledge_base_categories = params[:knowledge_base_categories] || 0
+    public_links              = params[:public_links] || 0
 
     puts 'load db with:'
+    puts " object_manager_attributes: #{object_manager_attributes}"
     puts " agents: #{agents}"
     puts " customers: #{customers}"
     puts " groups: #{groups}"
@@ -58,10 +74,65 @@ or if you only want to create 100 tickets
     puts " tickets: #{tickets}"
     puts " knowledge_base_answers: #{knowledge_base_answers}"
     puts " knowledge_base_categories: #{knowledge_base_categories}"
+    puts " public_links: #{public_links}"
 
     # set current user
     UserInfo.current_user_id = 1
 
+    # create object attributes
+    object_manager_attributes_value_lookup = {}
+    if object_manager_attributes.present?
+      ActiveRecord::Base.transaction do
+        object_manager_attributes.each do |object, attribute_types|
+          attribute_types.each do |attribute_type, amount|
+            next if amount.zero?
+
+            object_manager_attributes_value_lookup[object] ||= {}
+
+            amount.times do |index|
+              name = "#{attribute_type}_#{counter}"
+
+              object_attribute_creation = public_send("create_object_attribute_type_#{attribute_type}",
+                                                      object:     object,
+                                                      name:       name,
+                                                      display:    name,
+                                                      editable:   true,
+                                                      active:     true,
+                                                      screens:    {
+                                                        create_middle: {
+                                                          '-all-' => {
+                                                            shown:    true,
+                                                            required: false,
+                                                          },
+                                                        },
+                                                        create:        {
+                                                          '-all-' => {
+                                                            shown:    true,
+                                                            required: false,
+                                                          },
+                                                        },
+                                                        edit:          {
+                                                          '-all-' => {
+                                                            shown:    true,
+                                                            required: false,
+                                                          },
+                                                        },
+                                                      },
+                                                      to_migrate: true,
+                                                      to_delete:  false,
+                                                      to_config:  false,
+                                                      position:   1000 + index)
+
+              ObjectManager::Attribute.add(object_attribute_creation[:attribute_params])
+
+              object_manager_attributes_value_lookup[object][name] = object_attribute_creation[:value]
+            end
+          end
+        end
+        ObjectManager::Attribute.migration_execute(false)
+      end
+    end
+
     # organizations
     organization_pool = []
     if organizations.zero?
@@ -70,7 +141,16 @@ or if you only want to create 100 tickets
     else
       (1..organizations).each do
         ActiveRecord::Base.transaction do
-          organization = Organization.create!(name: "FillOrganization::#{counter}", active: true)
+          create_params = {
+            name:   "FillOrganization::#{counter}",
+            active: true
+          }
+
+          if object_manager_attributes_value_lookup[:organization].present?
+            create_params = create_params.merge(object_manager_attributes_value_lookup[:organization])
+          end
+
+          organization = Organization.create!(create_params)
           organization_pool.push organization
         end
       end
@@ -88,7 +168,8 @@ or if you only want to create 100 tickets
       (1..agents).each do
         ActiveRecord::Base.transaction do
           suffix = counter.to_s
-          user = User.create_or_update(
+
+          create_params = {
             login:     "filldb-agent-#{suffix}",
             firstname: "agent #{suffix}",
             lastname:  "agent #{suffix}",
@@ -97,7 +178,14 @@ or if you only want to create 100 tickets
             active:    true,
             roles:     roles,
             groups:    groups_all,
-          )
+          }
+
+          if object_manager_attributes_value_lookup[:user].present?
+            create_params = create_params.merge(object_manager_attributes_value_lookup[:user])
+          end
+
+          user = User.create_or_update(create_params)
+
           sleep nice
           agent_pool.push user
         end
@@ -122,7 +210,8 @@ or if you only want to create 100 tickets
           if organization_pool.present? && true_or_false.sample
             organization = organization_pool.sample
           end
-          user = User.create_or_update(
+
+          create_params = {
             login:        "filldb-customer-#{suffix}",
             firstname:    "customer #{suffix}",
             lastname:     "customer #{suffix}",
@@ -131,7 +220,14 @@ or if you only want to create 100 tickets
             active:       true,
             organization: organization,
             roles:        roles,
-          )
+          }
+
+          if object_manager_attributes_value_lookup[:user].present?
+            create_params = create_params.merge(object_manager_attributes_value_lookup[:user])
+          end
+
+          user = User.create_or_update(create_params)
+
           sleep nice
           customer_pool.push user
         end
@@ -147,7 +243,17 @@ or if you only want to create 100 tickets
     else
       (1..groups).each do
         ActiveRecord::Base.transaction do
-          group = Group.create!(name: "FillGroup::#{counter}", active: true)
+
+          create_params = {
+            name:   "FillGroup::#{counter}",
+            active: true,
+          }
+
+          if object_manager_attributes_value_lookup[:group].present?
+            create_params = create_params.merge(object_manager_attributes_value_lookup[:group])
+          end
+
+          group = Group.create!(create_params)
           group_pool.push group
           Role.where(name: 'Agent').first.users.where(active: true).each do |user|
             user_groups = user.groups
@@ -198,7 +304,8 @@ or if you only want to create 100 tickets
         ActiveRecord::Base.transaction do
           customer = customer_pool.sample
           agent    = agent_pool.sample
-          ticket = Ticket.create!(
+
+          create_params = {
             title:         "some title äöüß#{counter}",
             group:         group_pool.sample,
             customer:      customer,
@@ -207,7 +314,13 @@ or if you only want to create 100 tickets
             priority:      priority_pool.sample,
             updated_by_id: agent.id,
             created_by_id: agent.id,
-          )
+          }
+
+          if object_manager_attributes_value_lookup[:ticket].present?
+            create_params = create_params.merge(object_manager_attributes_value_lookup[:ticket])
+          end
+
+          ticket = Ticket.create!(create_params)
 
           # create article
           Ticket::Article.create!(
@@ -243,18 +356,28 @@ or if you only want to create 100 tickets
       end
     end
 
-    return if knowledge_base_answers.zero?
+    if knowledge_base_answers.positive?
+      ActiveRecord::Base.transaction do
+        create_knowledge_base_answers(
+          amount:            knowledge_base_answers,
+          categories_amount: knowledge_base_categories,
+          categories:        knowledge_base_categories_created,
+          knowledge_base:    knowledge_base,
+          agents:            agent_pool,
+          sleep_time:        nice,
+        )
+      end
+    end
+
+    return if public_links.zero?
 
     ActiveRecord::Base.transaction do
-      create_knowledge_base_answers(
-        amount:            knowledge_base_answers,
-        categories_amount: knowledge_base_categories,
-        categories:        knowledge_base_categories_created,
-        knowledge_base:    knowledge_base,
-        agents:            agent_pool,
-        sleep_time:        nice,
+      create_public_links(
+        amount:     public_links,
+        sleep_time: nice,
       )
     end
+
   end
 
   def self.counter
@@ -374,5 +497,273 @@ or if you only want to create 100 tickets
       sleep sleep_time
     end
   end
+
+  def self.create_public_links(params)
+    public_links_amount = params[:amount]
+    sleep_time = params[:sleep_time]
+
+    public_links_amount.times do |index|
+      public_link = PublicLink.create!(
+        title:         "Example#{counter}",
+        screen:        %w[login signup],
+        link:          "https://zammad#{counter}.com",
+        new_tab:       true,
+        prio:          index,
+        updated_by_id: 1,
+        created_by_id: 1,
+      )
+
+      puts " PublicLink #{public_link.id} created"
+
+      sleep sleep_time
+    end
+  end
+
+  def self.create_object_attribute_type_input(params)
+    {
+      attribute_params: params.merge(
+        data_type:   'input',
+        data_option: {
+          type:      'text',
+          maxlength: 200,
+          null:      true,
+          translate: false,
+        }
+      ),
+      value:            'example value',
+    }
+  end
+
+  def self.create_object_attribute_type_textarea(params)
+    {
+      attribute_params: params.merge(
+        data_type:   'textarea',
+        data_option: {
+          type:      'textarea',
+          maxlength: 200,
+          rows:      4,
+          null:      true,
+          translate: false,
+        }
+      ),
+      value:            "example value\nwith line break",
+    }
+  end
+
+  def self.create_object_attribute_type_integer(params)
+    {
+      attribute_params: params.merge(
+        data_type:   'integer',
+        data_option: {
+          default: 0,
+          null:    true,
+          min:     0,
+          max:     9999,
+        }
+      ),
+      value:            99,
+    }
+  end
+
+  def self.create_object_attribute_type_boolean(params)
+    {
+      attribute_params: params.merge(
+        data_type:   'boolean',
+        data_option: {
+          default: false,
+          null:    true,
+          options: {
+            true  => 'yes',
+            false => 'no',
+          }
+        }
+      ),
+      value:            true,
+    }
+  end
+
+  def self.create_object_attribute_type_date(params)
+    {
+      attribute_params: params.merge(
+        data_type:   'date',
+        data_option: {
+          diff: 24,
+          null: true,
+        }
+      ),
+      value:            '2022-12-01',
+    }
+  end
+
+  def self.create_object_attribute_type_datettime(params)
+    {
+      attribute_params: params.merge(
+        data_type:   'datetime',
+        data_option: {
+          diff:   24,
+          future: true,
+          past:   true,
+          null:   true,
+        }
+      ),
+      value:            '2022-10-01 12:00:00',
+    }
+  end
+
+  def self.create_object_attribute_type_select(params)
+    multiple = params[:data_type] == 'multiselect'
+
+    {
+      attribute_params: params.merge(
+        data_type:   params[:data_type] || 'select',
+        data_option: {
+          default:    multiple ? [] : '',
+          options:    {
+            'key_1' => 'value_1',
+            'key_2' => 'value_2',
+            'key_3' => 'value_3',
+            'key_4' => 'value_4',
+          },
+          multiple:   multiple,
+          translate:  true,
+          nulloption: true,
+          null:       true,
+        }
+      ),
+      value:            multiple ? %w[key_1 key_3] : 'key_3',
+    }
+  end
+
+  def self.create_object_attribute_type_multiselect(params)
+    create_object_attribute_type_select(
+      params.merge(
+        data_type: 'multiselect',
+      )
+    )
+  end
+
+  def self.create_object_attribute_type_tree_select(params)
+    multiple = params[:data_type] == 'multi_tree_select'
+
+    {
+      attribute_params: params.merge(
+        data_type:   params[:data_type] || 'tree_select',
+        data_option: {
+          default:    multiple ? [] : '',
+          options:    [
+            {
+              'name'     => 'Incident',
+              'value'    => 'Incident',
+              'children' => [
+                {
+                  'name'     => 'Hardware',
+                  'value'    => 'Incident::Hardware',
+                  'children' => [
+                    {
+                      'name'  => 'Monitor',
+                      'value' => 'Incident::Hardware::Monitor'
+                    },
+                    {
+                      'name'  => 'Mouse',
+                      'value' => 'Incident::Hardware::Mouse'
+                    },
+                    {
+                      'name'  => 'Keyboard',
+                      'value' => 'Incident::Hardware::Keyboard'
+                    }
+                  ]
+                },
+                {
+                  'name'     => 'Softwareproblem',
+                  'value'    => 'Incident::Softwareproblem',
+                  'children' => [
+                    {
+                      'name'  => 'CRM',
+                      'value' => 'Incident::Softwareproblem::CRM'
+                    },
+                    {
+                      'name'  => 'EDI',
+                      'value' => 'Incident::Softwareproblem::EDI'
+                    },
+                    {
+                      'name'     => 'SAP',
+                      'value'    => 'Incident::Softwareproblem::SAP',
+                      'children' => [
+                        {
+                          'name'  => 'Authentication',
+                          'value' => 'Incident::Softwareproblem::SAP::Authentication'
+                        },
+                        {
+                          'name'  => 'Not reachable',
+                          'value' => 'Incident::Softwareproblem::SAP::Not reachable'
+                        }
+                      ]
+                    },
+                    {
+                      'name'     => 'MS Office',
+                      'value'    => 'Incident::Softwareproblem::MS Office',
+                      'children' => [
+                        {
+                          'name'  => 'Excel',
+                          'value' => 'Incident::Softwareproblem::MS Office::Excel'
+                        },
+                        {
+                          'name'  => 'PowerPoint',
+                          'value' => 'Incident::Softwareproblem::MS Office::PowerPoint'
+                        },
+                        {
+                          'name'  => 'Word',
+                          'value' => 'Incident::Softwareproblem::MS Office::Word'
+                        },
+                        {
+                          'name'  => 'Outlook',
+                          'value' => 'Incident::Softwareproblem::MS Office::Outlook'
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              'name'     => 'Service request',
+              'value'    => 'Service request',
+              'children' => [
+                {
+                  'name'  => 'New software requirement',
+                  'value' => 'Service request::New software requirement'
+                },
+                {
+                  'name'  => 'New hardware',
+                  'value' => 'Service request::New hardware'
+                },
+                {
+                  'name'  => 'Consulting',
+                  'value' => 'Service request::Consulting'
+                }
+              ]
+            },
+            {
+              'name'  => 'Change request',
+              'value' => 'Change request'
+            }
+          ],
+          multiple:   multiple,
+          translate:  true,
+          nulloption: true,
+          null:       true,
+        }
+      ),
+      value:            multiple ? ['Change request', 'Incident::Hardware::Monitor', 'Incident::Softwareproblem::MS Office::Word'] : 'Incident::Hardware::Monitor',
+    }
+  end
+
+  def self.create_object_attribute_type_multi_tree_select(params)
+    create_object_attribute_type_tree_select(
+      params.merge(
+        data_type: 'multi_tree_select',
+      )
+    )
+  end
 end
 # rubocop:enable Rails/Output

+ 4 - 0
lib/tasks/zammad/db/pgloader.rake

@@ -0,0 +1,4 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require_dependency 'tasks/zammad/db/pgloader.rb'
+Tasks::Zammad::DB::Pgloader.register_rake_task

+ 115 - 0
lib/tasks/zammad/db/pgloader.rb

@@ -0,0 +1,115 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require_dependency 'tasks/zammad/command.rb'
+
+module Tasks
+  module Zammad
+    module DB
+      class Pgloader < Tasks::Zammad::Command
+
+        def self.description
+          'Prints out pgloader command file for the data migration from MySQL/MariaDB to PostgreSQL server.'
+        end
+
+        def self.task_handler
+          raise "Incorrect database configuration, expected `mysql2` for adapter but got `#{config['adapter']}`, check your database.yml!" if config['adapter'] != 'mysql2'
+
+          puts command_file
+        end
+
+        def self.command_file
+          <<~PGLOADER
+            LOAD DATABASE
+              FROM #{mysql_url}
+
+              -- Adjust the PostgreSQL URL below to correct value before executing this command file.
+              INTO pgsql://zammad:pgsql_password@localhost/zammad
+
+            ALTER SCHEMA '#{config['database']}' RENAME TO 'public'
+
+            AFTER LOAD DO
+            #{public_links.concat(object_manager_attributes).join(",\n")}
+
+            WITH BATCH CONCURRENCY = 1;
+          PGLOADER
+        end
+
+        # Generate URL of the source MySQL server:
+        #   mysql://[mysql_username[:mysql_password]@][mysql_host[:mysql_port]/][mysql_database]
+        def self.mysql_url
+          url = 'mysql://'
+
+          url += url_credentials(config['username'], config['password'])
+          url += url_hostname(config['host'], config['port'])
+          url += url_path(config['database'])
+
+          url
+        end
+
+        def self.config
+          return JSON.parse(ENV['ZAMMAD_TEST_DATABASE_CONFIG']) if ENV['ZAMMAD_TEST_DATABASE_CONFIG'].present?
+
+          Rails.configuration.database_configuration[Rails.env]
+        end
+
+        def self.url_credentials(username, password)
+          credentials = ''
+
+          if username.present?
+            credentials += username
+
+            if password.present?
+              credentials += ":#{password}"
+            end
+
+            credentials += '@'
+          end
+
+          credentials
+        end
+
+        def self.url_hostname(host, port)
+          hostname = ''
+
+          if host.present?
+            hostname += host
+
+            if port.present?
+              hostname += ":#{port}"
+            end
+
+            hostname += '/'
+          end
+
+          hostname
+        end
+
+        def self.url_path(database)
+          path = ''
+
+          if database.present?
+            path += database
+          end
+
+          path
+        end
+
+        def self.alter_table_command(table, column)
+          "  $$ alter table #{table} alter column #{column} type text[] using translate(#{column}::text, '[]', '{}')::text[] $$"
+        end
+
+        def self.object_manager_attributes
+          ObjectManager::Attribute.where(data_type: %w[multiselect multi_tree_select]).map do |field|
+            alter_table_command(field.object_lookup.name.constantize.table_name, field.name)
+          end
+        end
+
+        def self.public_links
+          [alter_table_command(PublicLink.table_name, 'screen')]
+        end
+
+        private_class_method :config, :url_credentials, :url_hostname, :url_path, :alter_table_command, :object_manager_attributes, :public_links
+      end
+    end
+  end
+end

+ 233 - 0
spec/lib/tasks/zammad/db/pgloader_spec.rb

@@ -0,0 +1,233 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe Tasks::Zammad::DB::Pgloader do
+  describe 'Task handler', db_adapter: :mysql, db_strategy: :reset_all do
+    let(:command_file) do
+      <<~PGLOADER
+        LOAD DATABASE
+          FROM #{mysql_url}
+
+          -- Adjust the PostgreSQL URL below to correct value before executing this command file.
+          INTO pgsql://zammad:pgsql_password@localhost/zammad
+
+        ALTER SCHEMA '#{config['database']}' RENAME TO 'public'
+
+        #{command_file_after}
+        WITH BATCH CONCURRENCY = 1;
+      PGLOADER
+    end
+
+    shared_examples 'preparing pgloader command file' do
+      it 'prepares pgloader command file' do
+        expect(described_class.command_file).to eq(command_file)
+      end
+    end
+
+    context 'with mysql adapter' do
+      let(:config)    { Rails.configuration.database_configuration[Rails.env] }
+      let(:mysql_url) { mysql_url_from_config(config) }
+
+      context 'with empty database' do
+        let(:command_file_after) do
+          <<~PGLOADER
+            AFTER LOAD DO
+              $$ alter table public_links alter column screen type text[] using translate(screen::text, '[]', '{}')::text[] $$
+          PGLOADER
+        end
+
+        it_behaves_like 'preparing pgloader command file'
+      end
+
+      context 'with multiselect and multi_tree_select object attributes' do
+        let(:command_file_after) do
+          <<~PGLOADER
+            AFTER LOAD DO
+              $$ alter table public_links alter column screen type text[] using translate(screen::text, '[]', '{}')::text[] $$,
+              $$ alter table #{object.table_name} alter column multi_select type text[] using translate(multi_select::text, '[]', '{}')::text[] $$,
+              $$ alter table #{object.table_name} alter column multi_tree_select type text[] using translate(multi_tree_select::text, '[]', '{}')::text[] $$
+          PGLOADER
+        end
+
+        before do
+          screens = { create_middle: { '-all-' => { shown: true, required: false } } }
+          create(:object_manager_attribute_multiselect, object_name: object.to_s, name: 'multi_select', display: 'Multi Select', screens: screens, additional_data_options: { options: { '1' => 'Option 1', '2' => 'Option 2', '3' => 'Option 3' } })
+          create(:object_manager_attribute_multi_tree_select, object_name: object.to_s, name: 'multi_tree_select', display: 'Multi Tree Select', screens: screens, additional_data_options: { options: [ { name: 'Parent 1', value: '1', children: [ { name: 'Option A', value: '1::a' }, { name: 'Option B', value: '1::b' } ] }, { name: 'Parent 2', value: '2', children: [ { name: 'Option C', value: '2::c' } ] }, { name: 'Option 3', value: '3' } ], default: '', null: true, relation: '', maxlength: 255, nulloption: true })
+        end
+
+        context 'with ticket object' do
+          let(:object) { Ticket }
+
+          it_behaves_like 'preparing pgloader command file'
+        end
+
+        context 'with user object' do
+          let(:object) { User }
+
+          it_behaves_like 'preparing pgloader command file'
+        end
+
+        context 'with organization object' do
+          let(:object) { Organization }
+
+          it_behaves_like 'preparing pgloader command file'
+        end
+
+        context 'with groups object' do
+          let(:object) { Group }
+
+          it_behaves_like 'preparing pgloader command file'
+        end
+      end
+    end
+
+    context 'without mysql adapter' do
+      before do
+        ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                             adapter: 'postgresql'
+                                                           })
+      end
+
+      it 'raises an error' do
+        expect { described_class.task_handler }.to raise_error('Incorrect database configuration, expected `mysql2` for adapter but got `postgresql`, check your database.yml!')
+      end
+    end
+
+    # Generate URL of the source MySQL server:
+    #   mysql://[mysql_username[:mysql_password]@][mysql_host[:mysql_port]/][mysql_database]
+    def mysql_url_from_config(config)
+      url = 'mysql://'
+
+      username = config['username']
+      password = config['password']
+      host     = config['host']
+      port     = config['port']
+      database = config['database']
+
+      if username.present?
+        url += username
+
+        if password.present?
+          url += ":#{password}"
+        end
+
+        url += '@'
+      end
+
+      if host.present?
+        url += host
+
+        if port.present?
+          url += ":#{port}"
+        end
+
+        url += '/'
+      end
+
+      if database.present?
+        url += database
+      end
+
+      url
+    end
+  end
+
+  describe 'MySQL URL generation' do
+    context 'with all attributes' do
+      before do
+        ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                             adapter:  'mysql',
+                                                             username: 'mysql_user',
+                                                             password: 'mysql_pass',
+                                                             host:     'mysql_host',
+                                                             port:     '3306',
+                                                             database: 'mysql_database',
+                                                           })
+      end
+
+      it 'returns full url' do
+        expect(described_class.mysql_url).to eq('mysql://mysql_user:mysql_pass@mysql_host:3306/mysql_database')
+      end
+    end
+
+    context 'without credentials' do
+      context 'without password' do
+        before do
+          ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                               adapter:  'mysql',
+                                                               username: 'mysql_user',
+                                                               host:     'mysql_host',
+                                                               port:     '3306',
+                                                               database: 'mysql_database',
+                                                             })
+        end
+
+        it 'returns url without password' do
+          expect(described_class.mysql_url).to eq('mysql://mysql_user@mysql_host:3306/mysql_database')
+        end
+      end
+
+      context 'without username' do
+        before do
+          ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                               adapter:  'mysql',
+                                                               password: 'mysql_password',
+                                                               host:     'mysql_host',
+                                                               port:     '3306',
+                                                               database: 'mysql_database',
+                                                             })
+        end
+
+        it 'returns url without credentials' do
+          expect(described_class.mysql_url).to eq('mysql://mysql_host:3306/mysql_database')
+        end
+      end
+    end
+
+    context 'without hostname' do
+      context 'without port' do
+        before do
+          ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                               adapter:  'mysql',
+                                                               host:     'mysql_host',
+                                                               database: 'mysql_database',
+                                                             })
+        end
+
+        it 'returns url without port' do
+          expect(described_class.mysql_url).to eq('mysql://mysql_host/mysql_database')
+        end
+      end
+
+      context 'without host' do
+        before do
+          ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                               adapter:  'mysql',
+                                                               port:     '3306',
+                                                               database: 'mysql_database',
+                                                             })
+        end
+
+        it 'returns url without hostname' do
+          expect(described_class.mysql_url).to eq('mysql://mysql_database')
+        end
+      end
+    end
+
+    context 'without path' do
+      context 'without database' do
+        before do
+          ENV['ZAMMAD_TEST_DATABASE_CONFIG'] = JSON.generate({
+                                                               adapter: 'mysql',
+                                                               host:    'mysql_host',
+                                                             })
+        end
+
+        it 'returns url without path' do
+          expect(described_class.mysql_url).to eq('mysql://mysql_host/')
+        end
+      end
+    end
+  end
+end