martes, 7 de junio de 2011

Ejecutar un archivo .sql desde PHP

Hace poco debí enfrentarme a este problema en el que debía ejecutar una actualización de un sistema, dentro de la actualización se hacían modificaciones a la estructura de la base de datos MySQL y no necesariamente se ejecutaba una consulta por actualización, sino que las consultas podían ser muchas.

Otro punto a detallar es que todas estas consultas venían dadas en un archivo .sql, porque podía ser usado además desde el CLI de MySQL o interfaces gráficas como MySQL Workbench o PHPMyAdmin. Y esto nos lleva a un problema, porque no podemos sólo leer todo el archivo y pasar el string obtenido como parametro a las funciones query de mysq, mysqli, o PDO ya que estos módulos ejecutan una consulta por vez.

Si el caso no hubiese sido el uso de .sql, una buena solución sería, por ejemplo, tener un array de PHP conteniendo todas las consultas separadas, incluir el archivo y luego iterar sobre el array ejecutando las consultas una a una.

Pero en el caso del .sql necesitamos separar las consultas que hay en el archivo para poder ejecutarlas una a una.
Una primera solución podría ser, sabiendo que en MySQL las consultas se separan por (;), leer el archivo a una variable, utilizar la función explode para separarlas por (;) y luego iterar sobre el arreglo resultante ejecutando las consultas una a una.

Ejemplo:



function ejecutarSQL($_rutaArchivo, $_conexionDB)
{
$queries = explode(';', file_get_contents($_rutaArchivo));
foreach($queries as $query)
{
if($query != '')
{
$_conexionDB->query($query): // Asumo un objeto conexión que ejecuta consultas
}
}
}


Esta aproximación sería correcta para archivos .sql como el siguiente:



ALTER TABLE tabla_a ADD COLUMN column_z TINYTEXT NOT NULL;
INSERT INTO tabla_b VALUES (1, 'a', 'valor');
CREATE TABLE tabla_c ( id INT auto_increment PRIMARY KEY, name VARCHAR(50) NOT NULL);

¿Pero qué pasa si el DBA que creo el archivo .sql correctamente agregó comentarios al archivo?



-- Agrego una columna para texto en tabla_a
ALTER TABLE tabla_a ADD COLUMN column_z TINYTEXT NOT NULL;
INSERT INTO tabla_b VALUES (1, 'a', 'valor');
-- Creo una tabla para mantener una lista de nombres
CREATE TABLE tabla_c ( id INT auto_increment PRIMARY KEY, nombre VARCHAR(50) NOT NULL);

Esto provocará que nuestro script falle, ya que encontrará en el string $query tokens que no pertenecen a una consulta correcta.
No es tan grave, se nos puede ocurrir utilizar expresiones regulares para identificar los comentarios y eliminarlos, y luego hacemos el explode. Solucionado.

Pero nada es fácil en la vida, porque: ¿qué sucede si en el archivo .sql nuestro querido amigo el DBA incluyó la creación de un stored procedure?
Esto nos quita toda posibilidad de usar nuestro script anterior, porque al crear un stored procedure necesitamos cambiar temporalmente el delimitador de las consultas, ya que las consultas dentro del stored procedure se delimitarán por (;).


Luego de darle unas vueltas a la idea llegué a la siguiente función, que quizás no sea la más óptima pero hasta ahora me funcionó en las situaciones en que debí usarlo.




function executeSqlScript($_db, $_fileName) {
$sql = file_get_contents($_fileName); // Leo el archivo
// Lo siguiente hace gran parte de la magia, nos devuelve todos los tokens no vacíos del archivo
$tokens = preg_split("/(--.*\s+|\s+|\/\*.*\*\/)/", $sql, null, PREG_SPLIT_NO_EMPTY);
$length = count($tokens);

$query = '';
$inSentence = false;
$curDelimiter = ";";
// Comienzo a recorrer el string
for($i = 0; $i < $length; $i++) {
$lower = strtolower($tokens[$i]);
$isStarter = in_array($lower, array( // Chequeo si el token actual es el comienzo de una consulta
'select', 'update', 'delete', 'insert',
'delimiter', 'create', 'alter', 'drop',
'call', 'set', 'use'
));

if($inSentence) { // Si estoy parseando una sentencia me fijo si lo que viene es un delimitador para terminar la consulta
if($tokens[$i] == $curDelimiter || substr(trim($tokens[$i]), -1*(strlen($curDelimiter))) == $curDelimiter) {
// Si terminamos el parseo ejecuto la consulta
$query .= str_replace($curDelimiter, '', $tokens[$i]); // Elimino el delimitador
$_db->query($query);
$query = ""; // Preparo la consulta para continuar con la siguiente sentencia
$tokens[$i] = '';
$inSentence = false;
}
}
else if($isStarter) { // Si hay que comenzar una consulta, verifico qué tipo de consulta es
// Si es delimitador, cambio el delimitador usado. No marco comienzo de secuencia porque el delimitador se encarga de eso en la próxima iteración
if($lower == 'delimiter' && isset($tokens[$i+1]))
$curDelimiter = $tokens[$i+1];
else
$inSentence = true; // Si no, comienzo una consulta
$query = "";
}
$query .= "{$tokens[$i]} "; // Voy acumulando los tokens en el string que contiene la consulta
}
}


La idea es separar el script sql en tokens utilizando una expresión regular que, además, está optimizada para que elimine los comentarios y utiliza un flag para descartar tokens vacios.
Luego se van recorriendo los tokens uno a uno identificando cuando hay un comienzo de sentencia sql y cuándo un final. Cuando la secuencia termina, se ejecuta y se vacia la cadena para recomenzar el ciclo

7 Comentários:

Unknown dijo...

muy buen aporte...
Gracias

Andrés Gustavo Muñoz Dávila dijo...

Muy buen aporte.

lo único que hice fue cambiar

$_db->query($query); por mysql_query($query, $_db)para que me funcione con mysql

ProPsycho dijo...

Muchas gracias Andrés.
Igualmente ten en cuenta que PHP recomienda no usar más las funciones de mysql, aconseja usar en su lugar mysqli o PDO.
Saludos y gracias por tu aporte.

Anónimo dijo...

a mi no me funciona

Elmer Guzman dijo...

eso puede funcionar con postgresql?

Elmer Guzman dijo...

Este codigo podria ser aplicado a una base postgresql?

ProPsycho dijo...

Hola, nunca hice la prueba.
Hice esta hace muchos años y seguramente ahora hay mil maneras mejores, te recomiendo intentar ejecutar el comando pgsql con la función exec.
Si no tienes otra alternativa, probablemente la expresión regular para identificar las sentencias sql tenga que ser cambiada, pero también debería funcionar con postgresql.

Publicar un comentario

Epistemomaniáticos ©Template Blogger Green by Dicas Blogger.

TOPO