PostgreSQL: Migrieren einer MySQL-Datenbank zu PostgreSQL am Beispiel Mantis Bug Tracker

Seit einigen Jahren arbeite ich mit mehreren Entwicklern, Testern und Benutzern an einem umfangreichen Softwareprojekt. Bei der Verfolgung von Bugs und Problemen leistet uns der Mantis Bug Tracker gute Dienste. Mantis basiert ursprünglich auf MySQL als Datenbank-Backend, allerdings ist in jüngerer Zeit die Unterstützung weiterer Datenbanken hinzugekommen.
Der zu diesem Zweck eingesetzte MySQL-Server fristet ein Exotendasein, da alle anderen Datenbankanwendungen im Intranet auf PostgreSQL basieren. So entstand der Wunsch, mit dem Upgrade auf eine neuere Version des Bugtrackers auch auf PostgreSQL als Backend umzusteigen - natürlich sollten dabei keine Daten verloren gehen. Was sich sehr simpel anhört (SQL ist ja schließlich SQL) gestaltete sich jedoch gar nicht so einfach…

Die Lösung

Bis alles auf dem PostgreSQL-Server lief, war viel Tüftelarbeit notwendig. Die beteiligten Computer (Server und Clients) liefen dabei alle unter Linux (Debian, openSUSE), wahrscheinlich funktionieren die Schritte aber grundsätzlich auch unter Windows (sed, grep und Konsorten machen das Leben allerdings um einiges leichter). Hier die einzelnen Schritte im Überblick:

  • Update der Mantis-Installation
  • Extrahieren eines PostgreSQL-kompatiblen Datenbank-Dumps aus der MySQL-Datenbank
  • Auswahl der relevanten SQL-Anweisungen aus dem Dump
  • Extrahieren der Binärdaten aus der MySQL-Datenbank
  • Anlegen der PostgreSQL-Datenbank und der entsprechenden Datenstrukturen
  • Einlesen aller Daten in die PostgreSQL-Datenbank

Update der Mantis-Installation

Zunächst aktualisiere ich die vorhandene (auf MySQL basierende) Mantis-Installation auf die neueste Version, denn damit wird auch die Datenbankstruktur auf den neuesten Stand gebracht. Die dafür notwendigen Schritte sind auf der Mantis-Homepage dokumentiert und lassen sich wie folgt zusammenfassen:

  • Entpacken des Installationsarchivs auf dem Server
  • Gegebenenfalls Anpassen der Konfigurationsdateien
  • Update der Datenbank durch Aufruf von http://[webserver]/admin/install.php

Extrahieren eines PostgreSQL-kompatiblen Datenbank-Dumps aus der MySQL-Datenbank

Grundsätzliches

Nun müssen die Daten aus der MySQL-Datenbank exportiert werden. Dafür würde man normalerweise das praktische Tool mysqldump verwenden. Das klappt hier jedoch nicht, da die Datenbankstrukturen nicht kompatibel sind. Vor allem bin ich über die folgenden Unterschiede gestolpert:

  • Mantis nutzt bei MySQL für Wahrheitswerte den Datentyp tinyint(4) und an selber Stelle bei PostgreSQL den Datentyp boolean (das heißt, in Feldern wo in der MySQL-Tabelle 0 oder 1 steht, muss bei PostgreSQL TRUE oder FALSE stehen)
  • ID's werden bei beiden Datenbanksystemen automatisch hochgezählt, allerdings mit unterschiedlichen Mechanismen. MySQL nutzt die Struktur-Anweisung AUTO_INCREMENT, bei PostgreSQL wird der Datentyp SERIAL verwendet (entspricht int und legt automatisch eine Sequenz als Default-Wert an)

Ein guter Startpunkt, um sich mit dieser Thematik vertraut zu machen, ist zum Beispiel dieser Wikibooks-Artikel. Es spricht nichts dagegen, einen MySQL-Dump direkt zu editieren und alle Umwandlungen manuell durchzuführen - wenn man viel viel Zeit hat. Auf der Wikibooks-Seite findet sich allerdings auch der Link zu mysql2postgres, einem Tool, das die entsprechenden Änderungen automatisch vornimmt (Download-Link).

mysql2postgres anpassen

Es handelt sich um ein Ruby-Skript, für das ich die folgenden Komponenten (unter openSUSE) installieren musste:

  • ruby
  • ruby-mysql
  • rubygems
  • rubygem-pg

Leider passte das Skript noch nicht so 100%ig für meine Zwecke, denn es wandelt lediglich tinyint(1)-Felder in boolean-Felder um. Bei meiner Mantis-Installation waren die Wahrheitswert-Felder jedoch vom Typ tinyint(4). Also musste ich das Skript ein wenig modifizieren, damit auch tinyint(4)-Felder zu boolean-Feldern umgewandelt werden.
Ich kenne mich (noch) überhaupt nicht mit Ruby aus, aber ziemlich am Anfang des Skripts gibt es einen Block (ab Zeile 90), der offensichtlich für die Übersetzung der Datentypen zuständig ist:

[...]
def convert_type(type)
  case type
  when /bigint/
    "bigint"
  when "bit(1)"
    "boolean"
  when "tinyint(1)"
    "boolean"
  when /tinyint/
    "tinyint"
  when /int/
    "integer"
  when /varchar/
    "varchar"
  when /char/
    "char"
  when /decimal/
    "decimal"
  when /double/
    "double precision"
  else
    type
  end.
end
[...]

Diesen Block konnte ich jedoch auch ohne Ruby-Kenntnisse um einen Abschnitt für tinyint(4) ergänzen:

Modifizierten Quelltext herunterladen: mysql2psql.modified

[...]
  when "tinyint(1)"
    "boolean"
  when "tinyint(4)"  #<----- neu eingefügt tinyint(4) -> boolean
    "boolean"        #<----- neu eingefügt tinyint(4) -> boolean
  when /tinyint/
    "tinyint"
[...]

mysql2postgres konfigurieren

Nun muss die Datei config.yml.sample nach config.yml kopiert und angepasst werden, indem man dort die Verbindungsdaten zur MySQL-Datenbank sowie einen Dateinamen für die Ausgabedatei (keine direkte Übertragung auf den PostgreSQL-Server!) einträgt. Außerdem muss die Tabelle mantis_bug_file_table vorerst ausgeschlossen werden, denn die Binärdaten müssen später gesondert behandelt werden. Einträge in eckigen Klammern [] müssen durch eigene Werte ersetzt werden.

config.yml
mysql:
 hostname: [Adresse des MySQL-Servers]
 port: 3306
 socket:
 username: [Benutzername]
 password: [Passwort]
 database: [Mantis-Datenbank]

destination:
 # if file is given, output goes to file, else postgres
 file: mantis_table_data_raw.sql
 postgres:
  hostname:
  port:
  username:
  password:
  database:

# if tables is given, only the listed tables will be converted. leave empty to convert all tables.
#tables:
#- table1
#- table2
#- table3
#- table4

# if exclude_tables is given, exclude the listed tables from the conversion.
exclude_tables:
- mantis_bug_file_table
#- table6

Wenn nun das modifizierte Ruby-Skript ausgeführt wird, dann dauert es nicht lange, bis der PostgreSQL-kompatible Datenbank-Dump in die Datei mantis_table_data.sql gewandert ist.

Auswahl der relevanten SQL-Anweisungen aus dem Dump

Obwohl das Skript die Datenbankstruktur ziemlich gut erkennt und umwandelt, mochte ich mich beim Anlegen der Tabellen, Indizes, Sequenzen und Constraints lieber auf die Installationsroutinen von Mantis verlassen. Dadurch wird sichergestellt, dass zumindest die Struktur so ist wie sie sein sollte und nichts beim Konvertieren verloren geht oder falsch interpretiert wird. Zunächst kopiere ich jedoch die vom Skript erzeugte Datei mantis_table_data_raw.sql in eine Arbeitskopie mantis_table_data.sql, um in dieser Kopie meine Anpassungen vornehmen zu können.

cp mantis_table_data_raw.sql mantis_table_data.sql

Der Datenbank-Dump ist so strukturiert, dass an erster Stelle alle SQL-Anweisungen stehen, mit denen Tabellen, Sequenzen und Indizes angelegt werden. Erst danach kommen alle Anweisungen zum Füllen der Tabellen. Da ich nur Daten importieren möchte (die Datenbank Struktur soll Mantis anlegen - siehe unten), lösche ich aus der Datei mantis_table_data.sql alle Zeilen bis zur ersten COPY-Anweisung (diese fängt bei mir mit COPY „mantis_bug_history_table“ […] an).

Beim Einrichten der Datenbank fügt Mantis in einige Tabellen bereits erste Datensätze ein. Die entsprechenden Tabellen müssen vor dem Einfügen der neuen Daten geleert werden, weil es sonst zu Konflikten (doppelte ID's) kommt. Die folgenden Zeilen müssen deshalb an den ANFANG der Datei mantis_table_data.sql kopiert werden:

DELETE FROM mantis_category_table;
DELETE FROM mantis_config_table;
DELETE FROM mantis_plugin_table;
DELETE FROM mantis_user_table;

Aber wozu gibt es denn den sed? Der Befehl

sed -i '1i\DELETE FROM mantis_category_table;\nDELETE FROM mantis_config_table;\nDELETE FROM mantis_plugin_table;\nDELETE FROM mantis_user_table;\n' mantis_table_data.sql

erledigt das direkt von der Kommandozeile aus.

Als nächstes suche ich aus der originalen Datei alle Sequenz-Zählerstände heraus und füge die SQL-Anweisungen an meine Arbeitskopie an. Natürlich nicht per Hand sondern mit dem Befehl

grep "SELECT pg_catalog.setval" mantis_table_data_raw.sql >> mantis_table_data.sql

Extrahieren der Binärdaten aus der MySQL-Datenbank

So, nun zu dem Teil, der mir die meisten Probleme bereitet hat. Mantis speichert hoch geladene Dateien direkt als Binärdaten in der Datenbank. Diese ließen sich, zumindest in meinem Fall, nicht direkt mit dem mysql2postgres-Skript von blob nach bytea konvertieren, denn irgendwie kam immer etwas mit den Byte-Sequenzen durcheinander.
Erst nach langen Experimenten fand ich heraus, wie ich die Tabelle mittels mysqldump extrahieren muss und welche Veränderungen an der SQL-Syntax mit sed vorgenommen werden müssen. Nämlich:

  • Beachte nur Zeilen mit INSERT-Anweisungen (die Datenbankstruktur legt Mantis an)
  • Byte-Daten müssen im Hexadezimal-Format und nicht als Escape-Character-Sequenz ausgegeben werden
  • Hochkommas um die Tabellen- und Spaltennamen müssen entfernt werden
  • Byte-Daten müssen vom Format '0xABCDEF0123456789' in das Format E'\xABCDEF0123456789' übertragen werden

Wenn man das weiß, ist der Rest schnell mit mysqldump und sed erledigt (Serveradresse, [Benutzername], [Passwort], [Datenbank] entsprechend ersetzen):

mysqldump -h[Serveradresse] -u[Benutzername] -p[Passwort] --database [Datenbank]  --table mantis_bug_file_table --skip-extended-insert --complete-insert --hex-blob | sed -n -e "/INSERT /s/\`//g" -e "s/',0x\([0-9A-Fa-f]*\),/',E'\\\\x\1\',/g p" >> mantis_table_data.sql

… und schon befindet sich auch der Inhalt der Tabelle mantis_bug_file_table in der Dump-Datei. Da die hexadezimale Darstellung der Binärdaten sehr viel Platz verschwendet, kann die Datei nun schon ziemlich groß sein (in meinem Fall mehr als 40 MB).

Zu guter Letzt wird noch der Zähler für die Sequenz mantis_bug_file_table_id_seq gesetzt. Diese war in bisher nicht im Dump enthalten, da die Tabelle mantis_bug_file_table beim mysql2postgres-Export ausgelassen wurde ([Serveradresse], [Benutzername], [Passwort], [Datenbank] entsprechend ersetzen).

echo "SELECT pg_catalog.setval('mantis_bug_file_table_id_seq', $(expr $(mysql -h[Serveradresse] -u[Benutzername] -p[Passwort] [Datenbank] --skip-column-names --batch -e "SELECT MAX(id) FROM mantis_bug_file_table;") + 1), true);" >> mantis_table_data.sql

Anlegen der PostgreSQL-Datenbank und der entsprechenden Datenstrukturen

Wie bereits erläutert, überlasse ich das Anlegen der Datenbankstrukturen auf dem PostgreSQL-Server Mantis. Dafür muss die Datei config_inc.php so angepasst werden, dass nun die PostgreSQL-Datenbank anstatt der MySQL-Datenbank verwendet wird (dokumentiert auf der Mantis-Webseite). Die Datenbank und der Datenbankbenutzer sollten bereits vorhanden sein. Danach muss man lediglich http://[webserver]/admin/install.php aufrufen, die entsprechenden Verbindungsdaten eintragen und den Rest erledigt Mantis wie von Zauberhand.

Einlesen aller Daten in die PostgreSQL-Datenbank

Nun ist das Ziel fast erreicht. Alle relevanten Daten befinden sich in der Datei mantis_table_data.sql. Mit dem folgenden Befehl werden sie an den PostgreSQL-Server übertragen:

psql -h [Serveradresse] -d [Datenbank] -U [Benutzername] -W --log-file log1.log -f mantis_table_data.sql &>log2.log

Fehler werden, falls welche auftreten, in der Datei log2.log dokumentiert. Bei mir waren (seltsamerweise) einige Bug-Beschreibungen zu lang und passten nicht in den varchar(128)-Datentyp. Das konnte ich aber leicht im Dump anpassen, indem ich die entsprechenden Beschreibungen kürzte. Zu beachten ist, dass wenn in einer COPY-Anweisung ein Fehler auftritt die Daten der gesamten Tabelle nicht eingelesen werden.
Da man den Dump auch nicht einfach ein zweites mal einlesen sollte (doppelte ID's etc.), habe ich die Datenbank stets gelöscht, neu angelegt und den geänderten Dump neu eingelesen bis keine Fehler mehr gemeldet wurden.

Falls MySQL- und PostgreSQL-Server unterschiedliche Zeichenkodierungen verwenden, so werden unter Umständen Umlaute nicht korrekt dargestellt. In diesem Fall sollte man an den Anfang der Dump-Datei die Anweisung

SET client_encoding = '[Kodierung der MySQL-Datenbank]';

setzen. In meinem Fall lief der MySQL-Server mit „Latin1“ und die PostgreSQL-Datenbank mit „UTF8“. Also setzte ich die Anweisung SET client_encoding = 'latin1'; an den Anfang meiner Datei.

Danach war es vollbracht! Ich konnte ich mich normal bei Mantis einloggen, alle Daten waren verfügbar und ich konnte ganz normal weiter arbeiten.