Aller au contenu
Porter une base PostGIS d'un PostgreSQL 15 vers un 9.6 : le sens interdit
  1. Posts/

Porter une base PostGIS d'un PostgreSQL 15 vers un 9.6 : le sens interdit

Fabien ALLAMANCHE
Auteur
Fabien ALLAMANCHE
Géomaticien @ Vienne Condrieu Agglomération
Sommaire

Introduction
#

Un besoin métier en apparence anodin peut révéler une dette technique profonde. Cet article raconte comment une simple demande — « rendre disponibles dans notre SIG web les données du réseau d’eau » — s’est transformée en un portage de base de données à contre-courant des versions.

Le décor : deux serveurs PostgreSQL/PostGIS, l’un moderne, l’autre très ancien, qu’il fallait synchroniser. Entre les deux, six versions majeures de PostgreSQL et une version majeure de PostGIS d’écart. Et surtout, un sens de migration que personne ne recommande : du récent vers l’ancien.

Vous allez suivre le cheminement complet : la réflexion d’architecture, le choix d’une stratégie, puis la mise en œuvre technique d’un script de synchronisation — avec les sept difficultés rencontrées et résolues une à une.

Table des matières
#

Contexte
#

Deux bases nommées stareau (implémentant le standard national StaR-Eau pour les réseaux d’eau potable et d’assainissement) devaient rester synchronisées entre deux infrastructures distinctes.

Info : Le standard StaR-Eau est un modèle de données normalisé pour la description des réseaux d’eau et d’assainissement. Une base stareau typique compte plusieurs dizaines de schémas thématiques et une centaine de tables.

Vous trouverez le dépôt officiel ici :

—r >}}

Le fossé technique entre les deux serveurs est le cœur du problème.

Composant Serveur A (interne) Serveur B (SIG web)
PostgreSQL 15.15 9.6.24 (fin de vie novembre 2021)
PostGIS 3.5.4 2.5.4 (hors support)
Système Rocky Linux 9.7 Ubuntu 18.04 (hors support)

Le serveur A est la source de vérité : c’est là que les données sont créées et structurées, via QGIS. Le serveur B alimente le SIG web, en consultation seule. L’objectif : propager régulièrement A vers B.

Attention : Le serveur cible est obsolète sur toute sa pile — système, base de données et cartouche spatiale. Ce n’est pas un détail : c’est ce qui interdit les solutions modernes et impose toute la suite.

Réflexion : quelle stratégie de synchronisation ?
#

Avant d’écrire la moindre ligne, il fallait choisir comment synchroniser. Le premier réflexe — une réplication bidirectionnelle où chacun écrit — est un piège.

Pourquoi pas de bidirectionnel
#

Si deux serveurs modifient indépendamment la même donnée, rien ne permet de trancher automatiquement laquelle fait foi au moment de réconcilier. PostgreSQL ne devine pas : il bloque la réplication pour éviter l’incohérence.

graph TB
    A["Serveur A : valeur X"]
    B["Serveur B : valeur Y"]
    A --> C{"Réconciliation :
qui fait foi ?"} B --> C C -->|"pas de résolution
de conflit native"| STOP["Réplication bloquée"] style A fill:#bbdefb,stroke:#1565c0,stroke-width:2px,color:#000 style B fill:#ffe0b2,stroke:#e65100,stroke-width:2px,color:#000 style C fill:#fff9c4,stroke:#f9a825,stroke-width:2px,color:#000 style STOP fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000

La règle d’or qui en découle : une seule source de vérité par table. Jamais la même table écrite des deux côtés.

Pourquoi pas de réplication native
#

Restait la réplication logique native de PostgreSQL. Mais elle n’existe qu’à partir de la version 10. Le serveur B, en 9.6, ne peut pas être abonné. La réplication physique, elle, exige des versions majeures identiques. Les deux sont éliminées d’office.

Alternative : Faute de mécanisme natif, il reste pg_dump / psql, qui fonctionne quelles que soient les versions. C’est rustique, mais c’est le seul chemin praticable tant que le serveur B n’est pas modernisé.

Le flux retenu est donc simple et unidirectionnel : on dumpe A, on adapte, on restaure sur B.

graph LR
    A[("Serveur A
PG 15 / PostGIS 3.5")] -->|"pg_dump"| D["Dump SQL"] D -->|"adaptation 15 → 9.6"| D2["Dump compatible 9.6"] D2 -->|"psql"| B[("Serveur B
PG 9.6 / PostGIS 2.5")] style A fill:#bbdefb,stroke:#1565c0,stroke-width:2px,color:#000 style B fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000 style D fill:#eeeeee,stroke:#616161,stroke-width:2px,color:#000 style D2 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,color:#000

Le portage 15 → 9.6 : sept difficultés
#

C’est ici que le « sens interdit » se paie. Chaque tentative de rejouer le dump de structure sur une base test 9.6 a buté sur une construction apparue dans une version PostgreSQL postérieure. Les voici dans l’ordre, avec pour chacune le symptôme, la cause et la correction.

Astuce : Travaillez toujours sur une base bac à sable jetable, et utilisez psql -v ON_ERROR_STOP=1. Le chargement s’arrête à la première erreur et vous indique précisément la ligne fautive — indispensable pour avancer méthodiquement.

1. Un paramètre de configuration inconnu
#

Erreur : paramètre de configuration « default_table_access_method » non reconnu

Ce paramètre est apparu en PostgreSQL 12. Le dump de la 15 l’écrit, la 9.6 ne le connaît pas. C’est purement cosmétique : on supprime la ligne.

sed -i "/^SET default_table_access_method/d" stareau_schema.sql

2. Le search_path vidé casse gen_random_uuid()
#

Erreur : la fonction gen_random_uuid() n'existe pas — alors qu’elle fonctionne en appel direct.

Piège subtil. pg_dump insère set_config('search_path', '', false) pour forcer les noms pleinement qualifiés. Résultat : la fonction gen_random_uuid(), native en PostgreSQL 13+ mais fournie par l’extension pgcrypto (dans public) en 9.6, n’est plus trouvée puisque public a quitté le chemin de recherche.

# On conserve public dans le search_path
sed -i "s/set_config('search_path', ''/set_config('search_path', 'public'/" stareau_schema.sql

3. Le type des séquences
#

Erreur : erreur de syntaxe sur ou près de « AS » dans un CREATE SEQUENCE.

La clause AS integer pour typer une séquence date de PostgreSQL 10. La 9.6 crée ses séquences en bigint implicite, ce qui reste compatible avec des colonnes id integer.

sed -i '/^    AS integer$/d' stareau_schema.sql
sed -i '/^    AS bigint$/d'  stareau_schema.sql

4. Les colonnes générées
#

Erreur : erreur de syntaxe sur ou près de « GENERATED ».

Les colonnes générées (GENERATED ALWAYS AS (...) STORED) sont apparues en PostgreSQL 12. Ici, quatre colonnes d’une table de points d’eau incendie calculaient des coordonnées (Lambert 93 et WGS84) depuis la géométrie.

La correction se fait en deux temps. D’abord, transformer ces colonnes en colonnes simples (leurs valeurs existantes viendront du dump de données) :

sed -i 's/    x_l93 numeric GENERATED ALWAYS AS.*STORED,/    x_l93 numeric,/' stareau_schema.sql
# … idem pour y_l93, lon, lat

Ensuite, recréer le calcul automatique via un trigger, qui est l’équivalent fonctionnel en 9.6 :

CREATE OR REPLACE FUNCTION defense_incendie.pei_calc_coords()
RETURNS trigger AS $$
BEGIN
  IF NEW.geom IS NOT NULL THEN
    NEW.x_l93 := ST_X(ST_Centroid(NEW.geom));
    NEW.y_l93 := ST_Y(ST_Centroid(NEW.geom));
    NEW.lon   := ST_X(ST_Transform(ST_Centroid(NEW.geom), 4326));
    NEW.lat   := ST_Y(ST_Transform(ST_Centroid(NEW.geom), 4326));
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_pei_calc_coords
  BEFORE INSERT OR UPDATE OF geom ON defense_incendie.pei
  FOR EACH ROW
  EXECUTE PROCEDURE defense_incendie.pei_calc_coords();

5. EXECUTE FUNCTION dans les triggers
#

Erreur : erreur de syntaxe sur ou près de « FUNCTION » dans un CREATE TRIGGER.

Le mot-clé EXECUTE FUNCTION date de PostgreSQL 11. La 9.6 n’accepte que l’ancienne forme EXECUTE PROCEDURE, au comportement identique. Un détail qui vaut aussi pour le trigger écrit à l’étape précédente.

sed -i 's/EXECUTE FUNCTION/EXECUTE PROCEDURE/g' stareau_schema.sql

6. Les extensions manquantes
#

Le schéma requiert pgcrypto (pour gen_random_uuid en 9.6) et hstore. Une comparaison des extensions installées de part et d’autre révèle un paradoxe instructif : pgcrypto est absente du serveur A… parce qu’elle y est native (PostgreSQL 15). Elle n’avait jamais eu besoin d’être installée. Sur la cible 9.6, il faut la créer explicitement.

psql -d stareau_test -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;"
psql -d stareau_test -c "CREATE EXTENSION IF NOT EXISTS hstore;"

7. Les domaines exclus du dump
#

Erreur : le type « public.c_insee » n'existe pas.

Deux domaines métier StaR-Eau — c_insee (un text avec contrainte de format sur le code INSEE) et c_annee — vivent dans le schéma public. Or le dump exclut public pour éviter d’emporter l’appareillage PostGIS 3.5 incompatible avec la 2.5. L’exclusion, trop large, laisse aussi ces domaines de côté.

La parade : les extraire à part et les charger avant la structure.

CREATE DOMAIN public.c_annee AS integer;
CREATE DOMAIN public.c_insee AS text
  CHECK (VALUE ~ '^([013-9]\d|2[AB1-9])\d{3}$');
Succès : Ordre de restauration validé — extensions → domaines → structure adaptée → données → trigger. Les données se chargent sans violation de clé étrangère : pg_dump ordonne correctement les tables.

Industrialisation dans un script
#

Une fois le portage validé à la main, il fallait le rendre reproductible. Le script pg_dump → adaptation → archivage → restauration reprend les conventions d’un script de sauvegarde existant (configuration sourcée, arborescence datée, rotation par rétention).

Deux principes de conception ont guidé le script.

graph TB
    subgraph "Phase 1-2 : préparation"
        P1["Découverte auto des schémas"]
        P2["Dump structure + données"]
        P3["7 adaptations 15 → 9.6"]
    end
    subgraph "Phase 3 : archivage"
        P4["Zip horodaté + rétention"]
    end
    subgraph "Phase 4 : restauration"
        P5{"Mode ?"}
        P6["test : base recréée"]
        P7["prod : purge + dump sécurité"]
    end
    P1 --> P2 --> P3 --> P4 --> P5
    P5 -->|"--restore test"| P6
    P5 -->|"--restore prod"| P7
    style P5 fill:#fff9c4,stroke:#f9a825,stroke-width:2px,color:#000
    style P6 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000
    style P7 fill:#ffe0b2,stroke:#e65100,stroke-width:2px,color:#000

La découverte automatique des schémas. Plutôt que de figer une liste, le script interroge le catalogue pour dumper tout schéma métier présent. Un nouveau schéma créé plus tard est ainsi pris en compte sans toucher au code.

SCHEMAS=$(psql -d stareau -At -c \
  "SELECT nspname FROM pg_namespace
   WHERE nspname NOT IN ('public','information_schema','topology','tiger','tiger_data')
     AND nspname NOT LIKE 'pg_%';")

Trois niveaux de sécurité de restauration. Sans option, le script se contente de dumper et d’archiver, sans jamais écrire sur la cible. --restore test vise une base bac à sable recréée à neuf. --restore prod est le seul mode qui touche la production, et il déclenche d’abord un dump de sécurité automatique.

Difficultés d’exploitation du script
#

Passer du fonctionnement manuel à l’automatisation a révélé une deuxième série de pièges, plus insidieux car liés à l’environnement.

Le fichier .pgpass
#

Deux erreurs classiques ont fait redemander le mot de passe malgré un .pgpass rempli. D’abord, le champ hôte doit correspondre littéralement à ce que passe l’option -h : une IP dans le fichier ne matche pas un localhost dans la commande. Ensuite, le mode restauration se connecte aussi à la base postgres (pour les DROP/CREATE DATABASE), pas seulement à stareau. La solution tient dans un wildcard sur la base :

10.10.22.100:5432:*:dyndb:MOTDEPASSE
195.42.149.140:5432:*:dyndb:MOTDEPASSE
Attention : En tâche planifiée (cron), $HOME peut différer et le fichier .pgpass ne pas être trouvé — le job se bloque sur une invite invisible. Fixez export PGPASSFILE="/chemin/.pgpass" dans la configuration, et gardez le fichier en chmod 600.

Le nettoyage garanti des fichiers temporaires
#

Le script laissait traîner les fichiers .sql volumineux à côté des archives. La correction robuste ne consiste pas à ajouter un rm en fin de script — qui serait sauté en cas d’erreur — mais à utiliser un trap qui s’exécute quoi qu’il arrive en sortie.

cleanup() {
    [ -n "$SCHEMA_RAW" ] && rm -f "$SCHEMA_RAW"
    [ -n "$SCHEMA_SQL" ] && rm -f "$SCHEMA_SQL"
    [ -n "$DATA_SQL" ]   && rm -f "$DATA_SQL"
}
trap cleanup EXIT

Restaurer sur une base déjà peuplée
#

Dernier obstacle, en mode production. Contrairement à la base test recréée vierge, la vraie base cible existe déjà et contient les objets. Les CREATE se heurtent à l’existant, avec un le type « c_annee » existe déjà.

Puisque le serveur A est la source de vérité et la cible en lecture seule, la stratégie retenue est de repartir de zéro : purger les schémas métier avant de recharger.

for s in $SCHEMAS; do
    psql -h "$DST_HOST" -U "$DST_USER" -d "$DB" \
         -c "DROP SCHEMA IF EXISTS \"$s\" CASCADE;"
done
Attention : DROP SCHEMA ... CASCADE supprime aussi tout objet externe qui en dépend — une vue applicative, par exemple. Avant de l’exécuter en production, vérifiez l’absence de telles dépendances en interrogeant pg_depend. Le dump de sécurité automatique reste le filet de dernier recours.

Vérification
#

Le contrôle final compare le nombre de tables par schéma entre la source et la cible.

psql -h 195.42.149.140 -U dyndb -d stareau -c "
SELECT schemaname, count(*) FROM pg_tables
WHERE schemaname NOT IN ('public','information_schema') AND schemaname NOT LIKE 'pg_%'
GROUP BY schemaname ORDER BY schemaname;"
Succès : 140 tables réparties à l’identique entre les deux serveurs, volumétrie confirmée sur les plus grosses tables. La base cible est une réplique fidèle de la source.

Conclusion
#

Ce qui ressemblait à une petite synchronisation s’est révélé un portage à contre-courant, où chaque objet un peu moderne demandait sa conversion.

Ce que vous retenez de ce retour d’expérience :

  • Le sens de migration récent → ancien n’est pas supporté : chaque construction postérieure à la version cible doit être adaptée à la main.
  • Une base « vidée » de ses schémas se restaure proprement, là où une base peuplée rejette les CREATE.
  • L’automatisation a ses propres pièges : .pgpass, $HOME en cron, nettoyage garanti par trap.
  • Travailler en bac à sable avec ON_ERROR_STOP=1 transforme un mur d’erreurs en une liste d’étapes.
Astuce : Toutes ces adaptations sont le coût récurrent de l’obsolescence du serveur cible. Le jour où il passe en version moderne, elles disparaissent et une réplication logique native devient possible. Le vrai chantier n’est pas le script : c’est la montée de version.

Articles connexes