domenica 30 gennaio 2011

Dati legacy: un piccolo dumper Perl per database PostgreSQL

Capita spesso di dover far dialogare la propria base dati PostgreSQL con un qualche tipo di applicazione legacy e/o proprietaria che non accetta nessuna forma di IPC o di data source configurabile. Solitamente in questi casi si è costretti a procedere attraverso l'import/export di file di testo in un qualche formato (CSV, tabulati, line-delimited, ecc.). Ebbene ultimamente ho dovuto anche io esportare dei dati "live" da un database PostgreSQL in modo che venissero riversati in un file di testo on-demand. Invece che scrivere un semplice script legato al contesto applicativo ho deciso di creare un mini driver piu' generale che permette un minimo di configurazione delle query e dei risultati. Ovviamente lo script Perl che propongo qui non è assolutamente un programma rock-solid, ma svolge abbastanza bene il suo compito; invito comunque chi abbia bisogno di estrazioni particolarmente complesse di costruire un qualche tipo di wrapper appositamente progettato.

Anzitutto ho deciso che ogni utente avrebbe potuto aver bisogno di query leggermente differenti (viste sui dati diverse), e quindi il componente dovrebbe poter caricare dinamicamente la query usando una qualche proprietà di ambiente, quale ad esempio la home dell'utente. Inoltre le query non possono avere i dati di selezione staticamente impostati, ma ci deve essere un qualche meccanismo di templating delle query: si deve poter specificare il tipo di query che l'utente vuole svolgere e questa deve essere effettivamente svolta con parametri passati dinamicamente. Insomma, una sorta di "prepared statement" del poveraccio!
Si immagini quindi di avere un file di testo, nella home utente (o in un qualche altro percorso parametrizzabile) che contenga il template di query:

SELECT pk, description
FROM   test
WHERE description like '{0}'
ORDER BY {1}



E' abbastanza ovvio che quello che si vuole ottenere è la sostituzione dei parametri posizionali {0} e {1} con valori reali e ottenuti dinamicamente all'atto dell'invocazione dell'estrattore. Segue lo script Perl che realizza questa funzione:


#!/usr/bin/perl

# variabili da utilizzare
$DATABASE = "mydb";
$HOST =  "localhost";
$MYSELF = "sqldump";
$QUERY_FILE =  $ENV{"HOME"} . "/$MYSELF" . "-" . "$DATABASE" . ".query";
$OUTPUT_FILE =  "/tmp/$MYSELF" . "-" . "$DATABASE" . ".txt";
$INTERNAL_FIELD_SEPARATOR = ";";
$FINAL_FIELD_SEPARATOR="\n";
$FINAL_RECORD_SEPARATOR=$FINAL_FIELD_SEPARATOR;



# lettura della query
# La query puo' essere parametrizzata con dei marcaposto {0} {1} ecc.
# Per ogni marcaposto ci deve essere un corrispondente parametro da linea di comando.
open( QUERY_FILE, "<" . "$QUERY_FILE" ) || die("\nImpossibile leggere il file di query\n$!\n");
while( $queryLine = ){
    chomp($queryLine);
    if( $queryLine =~ /\{(\d)\}/ ){

    $currentParam = $1;
    $queryLine =~ s/\{$currentParam\}/$ARGV[$currentParam]/g;
    }

   
    $query .= $queryLine . " ";

}


# Opzioni per l'esecuzione della query
$PSQL_OPTIONS = " -c \"$query\" -A -d $DATABASE  -F \'$INTERNAL_FIELD_SEPARATOR\' -h $HOST -t ";



# esecuzione del comando
open( OUTPUT, "psql $PSQL_OPTIONS |" ) || die("\nImpossibile eseguire il comando\n$!\n");

open( OUTPUT_FILE, ">" . $OUTPUT_FILE ) || die("\nImpossibile aprire il file in scrittura\n$!\n");

while( $sqlLine = ){

    chomp( $sqlLine );
    $sqlLine =~ s/$INTERNAL_FIELD_SEPARATOR/$FINAL_FIELD_SEPARATOR/g;
    $sqlLine .= $FINAL_RECORD_SEPARATOR;
    print OUTPUT_FILE $sqlLine;
}


Lo script è piuttosto semplice (come la maggior parte del codice Perl che scrivo!). La prima parte si preoccupa di impostare alcune variabili per puntare correttamente al database, usare un determinato tipo di delimitatore di record e campo, e cercare il file template della query nonché impostare il file dump testuale che verrà prodotto.
La prima parte di codice si occupa di ricostruire un'unica stringa di query SQL analizzando riga per riga l'eventuale presenza di parametri posizionali ({0},{1}, ecc.) e sostituendoli con il relativo parametro da riga di comando nella medesima posizione. Se ad esempio lo script viene invocato con:

sqldump.pl ciao description


la query SQL risultante sarà:

SELECT pk, description FROM   test WHERE description like 'ciao' ORDER BY description


Ancora una volta vale la pena ribadire la semplicità di questo approccio, che ben si presta a tecniche di SQL injection e ad altri problemi di sicurezza; ne consiglio ancora una volta il solo utilizzo in ambienti sicuri e comunque controllati.
Una volta ottenuta la query on-demand si puo' procedere all'interrogazione del database mediante le proprieta' di comunicazione con la shell del comando psql. In particolare al comando viene chiesto di eseguire la query in modalità di output non allineato, con un separatore di campo temporaneo. L'output della query viene passato mediante pipe al processo Perl (quindi si ha una fork del processo stesso) cosi' da poter gestire anche grosse moli di dati in uscita e senza avere ritardi dovuti alla produzione di file temporanei. Ogni riga di output viene poi ripulita e il separatore di campo e record corretti vengono applicati per la produzione dell'output testuale finale.
L'esecuzione batch di questo semplice script richiede la configurazione del file .pgpass per l'accesso ai database senza richiesta di username e password.
Questo script, ampiamente migliorabile, mostra come sia possibile gestire elasticamente l'input/output di un database PostgreSQL anche quando si sia forzati a usare soluzioni legacy come i file di testo. Ovviamente è possibile migliorare lo script stesso con una serie pressoché infinita di opzioni di configurazione.

Nessun commento: