Inspecter un catalogue avec pg_migrate
- 8 minutes de lecture
Au cours des dernières années, je me suis engagé à identifier les atouts et faiblesses d’outils dédiés à la migration vers PostgreSQL. Plusieurs articles publiés par mes soins ont pavé le chemin vers une direction ambitieuse que j’entreprends depuis 2024 avec mes collègues Étienne Bersac et Pierre-Louis Gonon.
… Et la version stable 1.0 de PostgreSQL Migrator est sortie le 4 septembre dernier. L’occasion de présenter des fonctionnalités que j’utilise au quotidien et ce qu’elles apportent par rapport aux autres outils. Dans cet article, je souhaite m’attarder sur l’une d’entre elles, particulièrement précieuse pour préparer une migration : le catalogue hors-ligne.
Consulter le catalogue Oracle
Le catalogue d’une base de données relationnelle renferme la structure du modèle, les noms et le typage des colonnes d’une table, les options de contraintes, la définition d’une vue ou d’une fonction, etc. Tout ce qui est déclaré par l’utilisateur avec du DDL (Data Definition Language) est maintenu dans un catalogue comme unique source de vérité.
Sur les systèmes comme PostgreSQL, MySQL ou MSSQL Server, la norme encourage
l’utilisation d’un catalogue universel, l’espace information_schema. Par
exemple, on peut y questionner les noms des tables avec la même requête :
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'scott'
ORDER BY table_name;
Cependant, ce n’est pas la solution idéale pour reconstruire un modèle de
données. Chacun des systèmes se conforment au mieux à la norme SQL mais très
fréquemment, ils l’enrichissent avec des extensions du langage ou prennent des
libertés sur l’implémentation d’une fonctionnalité. Il en résulte que le
catalogue information_schema n’est pas la source universelle, à peine une
collection d’alias dans un catalogue système propre à chacun.
À présent, parlons d’Oracle et d’Ora2Pg. Si nous souhaitons recréer la structure d’une table depuis l’écosystème Oracle, plusieurs méthodes existent et toutes s’appuient sur les vues du catalogue que je présente en dernier.
L’instruction DESCRIBE
Certainement la moins riche des solutions mais la plus rapide pour une première
inspection. Il s’agit d’un équivalent de la méta-commande \d dans psql ou du
pragma table_info pour SQLite.
DESCRIBE SCOTT.EMP;
-- Name Null? Type
-- -------- -------- ------------
-- EMPNO NOT NULL NUMBER(4)
-- ENAME VARCHAR2(10)
-- JOB VARCHAR2(9)
-- MGR NUMBER(4)
-- HIREDATE DATE
-- SAL NUMBER(7,2)
-- COMM NUMBER(7,2)
-- DEPTNO NUMBER(2)
L’API METADATA
Il est possible d’employer un package Oracle très utile pour générer la
structure d’une table au format texte SQL. Les équivalents des autres systèmes
seraient l’instruction SHOW CREATE TABLE de MySQL ou encore .schema de
SQLite.
SET pagesize 0
SET long 20000
SELECT dbms_metadata.get_ddl(
object_type => 'TABLE',
schema => 'SCOTT',
name => 'EMP');
-- CREATE TABLE "SCOTT"."EMP"
-- ( "EMPNO" NUMBER(4,0),
-- "ENAME" VARCHAR2(10),
-- "JOB" VARCHAR2(9),
-- "MGR" NUMBER(4,0),
-- "HIREDATE" DATE,
-- "SAL" NUMBER(7,2),
-- "COMM" NUMBER(7,2),
-- "DEPTNO" NUMBER(2,0),
-- CONSTRAINT "PK_EMP" PRIMARY KEY ("EMPNO")
-- USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255
-- STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
-- PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
-- TABLESPACE "SYSTEM" ENABLE,
-- CONSTRAINT "FK_DEPTNO" FOREIGN KEY ("DEPTNO")
-- REFERENCES "SCOTT"."DEPT" ("DEPTNO") ENABLE
-- ) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
-- STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
-- PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
-- TABLESPACE "SYSTEM"
Les vues du catalogue
Les résultats précédents sont compilés depuis plusieurs vues systèmes. Sans surprise, les explorateurs classiques comme SQL Developer ou DBeaver, ainsi qu’Ora2Pg et PostgreSQL Migrator, consultent directement les données des différentes vues pour obtenir le DDL complet.
Ora2Pg obtient la liste des tables depuis la vue ALL_OBJECTS et le stocke en
mémoire dans un tableau associatif $self->{tables}. Puis, il itère sur
ALL_TAB_COLUMNS pour collecter le détail de chaque table et les rattacher avec
une nouvelle profondeur de ce même tableau global
$self->{tables}{$table}{column_info}{$column}.
SELECT A.OWNER,A.OBJECT_NAME,A.OBJECT_TYPE FROM ALL_OBJECTS A
WHERE A.OBJECT_TYPE IN ('TABLE','VIEW') AND A.OWNER='SCOTT';
SELECT A.COLUMN_NAME, A.DATA_TYPE, A.DATA_LENGTH, A.NULLABLE, A.DATA_DEFAULT,
A.DATA_PRECISION, A.DATA_SCALE, A.CHAR_LENGTH, A.TABLE_NAME, A.OWNER
FROM ALL_TAB_COLUMNS A
WHERE A.TABLE_NAME NOT LIKE 'BIN$%' AND A.TABLE_NAME='EMP'
ORDER BY A.COLUMN_ID
La structure en mémoire $self que maintient Ora2Pg ne persiste pas entre deux
exécutions. La lecture du catalogue source est nécessaire à chaque fois. En
opposition, j’ai eu l’occasion de découvrir une alternative nommée
db_migrator, basée sur les Foreign Data Wrapper. Avec cette solution,
le catalogue est stockée dans une base de données PostgreSQL.
Il était alors simple de lancer une inspection avec la méthode
db_migrate_refresh() pour créer un instantané du catalogue distant et de
pouvoir le questionner hors-ligne, sans se reconnecter à l’instance Oracle. De
cette découverte émergera les premiers tâtonnements du catalogue au format JSON
de PostgreSQL Migrator.
Questionner du JSON
Le stockage du catalogue requiert un format structuré, stable et non contraignant. De facto, le choix de conserver les données dans des tables PostgreSQL nous est apparu inadapté, bien qu’élégant pour des yeux de DBA. L’outil PostgreSQL Migrator se vante d’être portable et nous ne souhaitions pas encombrer l’instance PostgreSQL finale d’un schéma transitoire.
Comme Ora2Pg, une structure mémoire Catalog (internal/catalog/catalog.go)
est initialisée au début de l’inspection. Ce catalogue peut être étendu en
fonction que la source soit une base Oracle ou MySQL/MariaDB, pour collecter les
objets spécifiques du moteur, comme les packages ou les synonymes. Dès lors
que la lecture du catalogue distant est réalisée, il suffit de sérialiser la
structure dans un fichier JSON. Actuellement, la version 1.0 s’appuit sur le
module sonic.
L’inspection est la première étape pour démarrer un projet avec pg_migrate, le
petit nom de l’interface en ligne de commande (CLI) de PostgreSQL Migrator.
Après avoir initialisé un dossier vide et valider l’accès à la base distante
Oracle, les traces nous invitent à exécuter la commande pg_migrate inspect.
$ ora-scott && cd ora-scott
$ pg_migrate init --source "oracle://system:manager@localhost:1521/freepdb1"
14:55:28 INFO Databases connections closed. source=1 target=0
14:55:28 INFO Initialized migration project. source="Oracle Database 23ai Free"
14:55:28 INFO Inspect source catalog using pg_migrate inspect.
$ pg_migrate --verbose inspect
14:56:03 DEBUG Cache miss. name=Source
14:56:03 INFO Inspecting source database. driver=oracle
14:56:03 INFO This may take some time.
14:56:04 DEBUG Inspected schemas. count=1 duration=84.735201ms
14:56:05 DEBUG Inspected roles. count=6 duration=465.814794ms
14:56:06 DEBUG Inspected sequences. count=0 duration=1.050025889s
14:56:07 DEBUG Inspected routines. count=0 duration=2.794483231s
14:56:07 DEBUG Inspected views. count=0 duration=2.63147026s
14:56:21 DEBUG Inspected tables. count=4 duration=15.720365706s
14:56:31 DEBUG Inspected columns. count=18 duration=10.735219841s
14:56:41 DEBUG Inspected keys. count=2 duration=19.945416304s
14:56:44 DEBUG Inspected tables-indexes. count=0 duration=23.287211484s
14:56:48 DEBUG Inspected foreign-keys. count=1 duration=6.525976802s
14:56:58 DEBUG Inspected checks. count=0 duration=10.622190194s
14:56:58 INFO Inspected schema. name=SCOTT
14:56:58 DEBUG Inspection completed. duration=54.968668546s
...
14:57:12 INFO Inspection terminated. errs=0 online=true
14:57:12 INFO Execute pg_migrate ui to browse project.
14:57:12 INFO Execute pg_migrate dump to migrate for PostgreSQL.
Lors d’une première exécution, le catalogue local est inexistant (Cache miss. name=Source), ce qui provoque une connexion à la base distante pour le créer de
toute pièce. Les fichiers de cache sont écrits dans le dossier caché
.pg_migrate. Le catalogue de la source se nomme sobrement Source.json.
$ tree .pg_migrate/
.pg_migrate
├── Info.json
├── Map.json
├── Source.json
├── Stats.json
└── Target.json
Il a alors fallu me faire violence pour mettre de côté mes précieuses
connaissances en SQL pour apprendre le langage jq afin de questionner les
fichiers JSON d’un projet pg_migrate. Par exemple, le filtre suivant liste
toutes les colonnes et leurs types de la table SCOTT.EMP :
$ set filter '.Tables[] | select (.Name == "EMP") | .Columns[] | .objectDefinition'
$ jq -r $filter < .pg_migrate/Source.json
EMPNO NUMBER(4, 0) NOT NULL
ENAME VARCHAR2(10)
JOB VARCHAR2(9)
MGR NUMBER(4, 0)
HIREDATE DATE
SAL NUMBER(7, 2)
COMM NUMBER(7, 2)
DEPTNO NUMBER(2, 0)
Bien évidemment, l’outil a fait un très long chemin depuis 2024 et cette architecture de catalogue hors-ligne est devenue la colonne vertébrale du produit. Ces fichiers ont permis d’explorer des possibilités jusqu’ici inatteignables avec Ora2Pg, en plus du mode hors-ligne.
- La conversion passe d’un fichier
Source.jsonà un fichierTarget.jsonen lui appliquant des règles de conversion génériques et spécifiques ; - Une phase d’audit parcourt tous les nœuds pour appliquer un score, voire des annotations sur les difficultés de conversion ;
- Une interface Web de type SPA (single page app) permet d’explorer les deux
modèles (source et cible) pour se passer des filtres
jq.
L’outil vient avec une commande dump, que l’on peut détourner pour récupérer
la forme DDL d’un objet stocké dans le Target.json. Pour le moment, la
commande complète est la suivante :
$ pg_migrate dump --force --section=pre-data Tables/scott.emp 2>/dev/null | cat
--
-- Name: emp; Type: TABLE; Schema: scott; Owner: scott
--
CREATE TABLE "scott"."emp" (
"empno" smallint NOT NULL,
"ename" varchar(10),
"job" varchar(9),
"mgr" smallint,
"hiredate" timestamp,
"sal" numeric(7, 2),
"comm" numeric(7, 2),
"deptno" smallint
);
ALTER TABLE "scott"."emp" OWNER TO "scott";
Conclusion
Je suis profondément fier du travail accompli avec cette première version stable. Nous concluons une aventure palpitante avec mes collègues chez Dalibo, celle de plus de deux ans de développement d’un outil libre et gratuit et qui a vocation à dépasser son illustre prédécesseur Ora2Pg. La feuille de route est claire. Nous avons un retard métier conséquent sur la référence open-source de la migration, mais nous ne rougissont pas. Les fondations sont solides, le langage Go permet de répondre à de véritables besoins de simplification et de modernité.
À l’heure où les empires du numérique se gavent de nos données métiers, qu’ils siphonnent la valeur travail de nos entreprises, il est urgent de réagir et de croire qu’un autre numérique est possible. Les projets de migration deviennent complexes, exigeants, avec des profils très variés, nous tentons d’apporter une réponse qui ne repose pas exclusivement sur des agents LLM ou des services américains.
La voie du logiciel libre est devant nous depuis des décennies, il ne manque que les passerelles qui permettent de tourner le dos à ces technologies privatives de liberté, d’innovation et de moyens. À mon échelle de DBA et de Product Owner, engagé dans la coopérative Dalibo, j’œuvre pour cette urgence collective.