-- =============================================================================
-- 03_convert_partition_avec_donnees.sql — migre UNE partition contenant des
-- données, de HASH×16 vers RANGE simple, sans perte.
--
-- À lancer une partition à la fois, en passant son nom :
--   docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \
--     -v ON_ERROR_STOP=1 -v part=positions_2026_w31 \
--     < 03_convert_partition_avec_donnees.sql
--
-- PRÉALABLE OBLIGATOIRE — sauvegarde de la partition concernée :
--   docker exec wordpress-postgres pg_dump -U pgz_admin -d wpz_postgres \
--     -t "adsb.<partition>*" -Fc -f /tmp/<partition>.dump
--   docker cp wordpress-postgres:/tmp/<partition>.dump /data/adsb/backups/
--
-- Principe :
--   1. DETACH de l'ancienne partition (les données restent, hors du parent)
--   2. CREATE de la nouvelle partition RANGE simple, attachée
--   3. INSERT ... SELECT depuis l'ancienne vers le parent
--   4. Contrôle des comptes AVANT/APRÈS — DROP uniquement si identiques
--
-- Le tout dans UNE transaction : en cas de problème, ROLLBACK ramène l'état
-- initial. Pendant l'opération, adsb2pg peut continuer d'insérer : ses lignes
-- arrivent dans la nouvelle partition, déjà attachée.
--
-- NOTE : sur la partition COURANTE, faire l'opération à faible trafic. Les
-- lignes insérées entre le DETACH et le CREATE (quelques millisecondes)
-- provoqueraient une erreur « no partition of relation found » côté adsb2pg,
-- qui les réessaiera au cycle suivant.
-- =============================================================================

\timing on
\set ON_ERROR_STOP on

\if :{?part}
\else
  \echo 'ERREUR : passer -v part=<nom_partition>'
  \quit 1
\endif

BEGIN;

-- Verrou explicite : empêche toute modification concurrente de la structure
LOCK TABLE adsb.positions IN SHARE UPDATE EXCLUSIVE MODE;

-- Transmission du nom de partition au bloc PL/pgSQL (les variables psql ne
-- sont pas visibles depuis un DO $$ : il faut passer par un paramètre de
-- session).
SET LOCAL my.part = :'part';

DO $$
DECLARE
  p_old      text := current_setting('my.part');
  p_tmp      text := current_setting('my.part') || '_old';
  bornes     text;
  d0         date;
  d1         date;
  n_avant    bigint;
  n_apres    bigint;
  cols       text;
BEGIN
  SELECT pg_get_expr(c.relpartbound, c.oid) INTO bornes
  FROM pg_class c JOIN pg_namespace ns ON ns.oid = c.relnamespace
  WHERE ns.nspname = 'adsb' AND c.relname = p_old;

  IF bornes IS NULL THEN
    RAISE EXCEPTION 'Partition adsb.% introuvable ou non attachée', p_old;
  END IF;

  d0 := (regexp_match(bornes, 'FROM \(''([0-9-]+)'))[1]::date;
  d1 := (regexp_match(bornes, 'TO \(''([0-9-]+)'))[1]::date;

  EXECUTE format('SELECT count(*) FROM adsb.%I', p_old) INTO n_avant;
  RAISE NOTICE 'Partition % : % ligne(s) à migrer (bornes % → %)', p_old, n_avant, d0, d1;

  -- 1. Détacher et renommer l'ancienne
  EXECUTE format('ALTER TABLE adsb.positions DETACH PARTITION adsb.%I', p_old);
  EXECUTE format('ALTER TABLE adsb.%I RENAME TO %I', p_old, p_tmp);

  -- 2. Créer la nouvelle, en RANGE simple
  EXECUTE format(
    'CREATE TABLE adsb.%I PARTITION OF adsb.positions FOR VALUES FROM (%L) TO (%L)',
    p_old, d0, d1);

  -- 3. Recopier les données.
  --    geom est GENERATED ALWAYS : PostgreSQL refuse toute valeur explicite
  --    (« cannot insert a non-DEFAULT value into column geom »). Un SELECT *
  --    la remonterait, il faut donc énumérer les colonnes réelles et l'exclure.
  --    Elle sera recalculée automatiquement depuis lat/lon à l'insertion.
  SELECT string_agg(quote_ident(a.attname), ', ' ORDER BY a.attnum)
    INTO cols
  FROM pg_attribute a
  WHERE a.attrelid = format('adsb.%I', p_tmp)::regclass
    AND a.attnum > 0
    AND NOT a.attisdropped
    AND a.attgenerated = '';   -- exclut les colonnes générées

  IF cols IS NULL OR cols = '' THEN
    RAISE EXCEPTION 'Impossible de déterminer les colonnes de adsb.%', p_tmp;
  END IF;

  RAISE NOTICE 'Colonnes copiées (geom exclue, recalculée) : %', cols;

  EXECUTE format('INSERT INTO adsb.%I (%s) SELECT %s FROM adsb.%I',
                 p_old, cols, cols, p_tmp);

  EXECUTE format('SELECT count(*) FROM adsb.%I', p_old) INTO n_apres;

  -- 4. Contrôle : on ne supprime QUE si le compte correspond exactement.
  IF n_apres <> n_avant THEN
    RAISE EXCEPTION 'ÉCART DE COMPTE : % avant, % après — transaction annulée, aucune donnée perdue',
                    n_avant, n_apres;
  END IF;

  RAISE NOTICE 'Copie vérifiée : % ligne(s) — suppression de l''ancienne structure', n_apres;
  EXECUTE format('DROP TABLE adsb.%I', p_tmp);

  -- 5. Index sur la nouvelle partition
  EXECUTE format('CREATE INDEX %I ON adsb.%I USING GIST (geom)', p_old||'_geom_gix', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (ts DESC)', p_old||'_ts_idx', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (icao, ts DESC)', p_old||'_icao_ts_idx', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (inserted_at DESC)', p_old||'_inserted_at_idx', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (callsign) WHERE callsign IS NOT NULL',
                 p_old||'_callsign_idx', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (snap_id)', p_old||'_snap_id_idx', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (receiver_id)', p_old||'_receiver_id_idx', p_old);
  EXECUTE format('CREATE INDEX %I ON adsb.%I (aircraft_wtc)', p_old||'_aircraft_wtc_idx', p_old);
  EXECUTE format(
    'ALTER TABLE adsb.%I ADD CONSTRAINT %I FOREIGN KEY (receiver_id) '
    'REFERENCES adsb.receivers(id) ON DELETE SET NULL',
    p_old, p_old||'_receiver_fk');

  RAISE NOTICE 'Partition % convertie en RANGE simple avec ses index', p_old;
END $$;

COMMIT;

-- ANALYZE hors transaction : indispensable, la nouvelle partition n'a aucune
-- statistique et le planner ferait de mauvais choix.
ANALYZE adsb.positions;

\timing off
