somesqldump (generador de datos para testear bases de datos mysql)
Nombre: somesqldump
Autor: Patricio Silva (Pato Silva)
Lenguaje: perl
Licencia: GPL
Dependencias: perl
Propósito: Genera una salida correcta similar a la que generaria mysqldump pero con datos aleatorios (o reales) para testeo de bases de datos. Puede generear sin problemas millones de regitros para varias tablas respetando condiciones como indices únicos, foreign keys o exclusion mutua de valores.
Pueden crearse campos con nombres, apellidos, calles o nombres de ciudades o empresas reales. La sintaxis que debe pasarse para generar los resultados es muy similar a una sentencia INSERT.
Lo he probado generado varias tablas de 10 millones de registros con foreign keys y ha funcionado correctamente, el resultado fueron mas de 3 GB de datos generados en mas o menos media hora (y unas 15 horas para cargarlos en la base de datos).
Agrega automaticamente las sentencias LOCK TABLE/UNLOCK TABLES y DISABLE KEYS/ENABLE KEYS para acelerar la carga.
Código:
#!/usr/bin/perl
# @author Pato Silva
# @date 23 de febrero de 2009
# @version 1.0
use strict;
require "somesqldata.pl";
srand();
our @chars = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
my $time_stamp = time();
my $temp_directory = "/tmp/";
my $size = 0;
my $table_name = "";
my $param_separator = ', '; # Separador de parametros
my $param_quote = "'"; # Caracter para entrocomillar caracteres
my @args; # Argumentos del guion de entrada en crudo (cada argumento genera un arreglo @cmds)
my @cmds; # Comandos del guion de entrada
my @cmds_index; # Indide, es un numero que depende del comando
my @cmds_bottom; # Si el comando requiere de un valor inferior se almacena aqui
my @cmds_top; # Si el comando requiere de un valor superior se almacena aqui
my @cmds_data; # Si el comando requiere de algun valor almacenado. Lo que se almacene aqui depende del comando
my $global_count = 0; # Conteo global de registros generados
my $errors = 0; # Conteo de errores
my $warnings = 0; # Conteo de advertencias
my $parse_only = 0;
# mezcla el array pasado por parametro
sub mix{
my $array = shift;
for(my $i = 0 ; $i < scalar(@{$array}); $i++){
my $random = int(rand(scalar@{$array}));
my $aux = ${$array}[$i];
${$array}[$i] = ${$array}[$random];
${$array}[$random] = $aux;
}
}
# Retorna un string de entre $length_min y $length_max caracteres, $type indica la cantidad de caracteres a incluir
# si vale 25 incluye solo letras minusculas, 51 incluye minusculas y maysculas, 61 incluye numeros
# los valores de 61 en adelante agregan espacios (cuanto mayor es mas aumenta la probabilidad de elegir un espacio)
sub get_random_char{
my $length_min = shift;
my $length_max = shift;
my $type = shift;
our @chars;
my $length = int(rand($length_max - $length_min)) + $length_min + 1;
my $word = '';
for(my $j = 0 ; $j < $length; $j++){
my $random = int(rand($type));
$word .= ($random > 61)?' ':$chars[$random];
}
return $word;
}
# Retorna un entero al azar cuyo valor está entre $min y $max
sub get_random_int{
my $min = shift;
my $max = shift;
return int(rand($max-$min+1)) + $min;
}
# Retorna un array con elementos unicos de tipo entero, de size elementos
# cuyos valores están entre $min y $max.
sub get_unique_integer_random_list{
my $size = shift;
my $min = shift;
my $max = shift;
my $increment = ($max - $min)/$size;
my $last = $min;
my $new = 0;
my $count = 0;
my @list = ();
while($new <= $max){
$new = $last + int(rand($increment)+1);
push(@list, $new);
$last = $new;
}
pop(@list);
&mix(\@list);
splice(@list, 0, -$size);
return @list;
}
# Retorna un nombre al azar
sub get_random_name{
our @NOMBRES;
return $NOMBRES[int(rand(scalar(@NOMBRES)))];
}
# Retorna un apellido al azar
sub get_random_last_name{
our @APELLIDOS;
return $APELLIDOS[int(rand(scalar(@APELLIDOS)))];
}
# Retorna el nombre de una calle al azar
sub get_random_street{
our @CALLES;
return $CALLES[int(rand(scalar(@CALLES)))];
}
# Retorna un nombre de empresa al azar
sub get_random_company{
our @EMPRESAS;
return $EMPRESAS[int(rand(scalar(@EMPRESAS)))];
}
# Retorna un nombre de ciudad al azar
sub get_random_city{
our @CIUDADES;
return $CIUDADES[int(rand(scalar(@CIUDADES)))];
}
if($#ARGV == -1){
while(my $line = <STDIN>){
chomp($line);
if(length($line) > 0){ push(@args, $line); }
}
}else{
@args = @ARGV;
}
# Parser
for (my $i = 0 ; $i < scalar(@args) ; $i++){
if($args[$i] =~ /^\s*INSERT\s+\d+\s+INTO\s+\w+\s+VALUES\s*\(.+\);\s*$/){
print STDERR "\n----------------------------\nParser:[ " . $args[$i] . " ]: parece correcto.\n----------------------------\n";
@cmds = split(/^\s*INSERT\s+|\s+INTO\s+|\s+VALUES\s*\(\s*|\s*,\s+|\s*\);\s*$/, $args[$i]);
@cmds_index = (0..scalar(@cmds)-4);
@cmds_bottom = (0..scalar(@cmds)-4);
@cmds_top = (0..scalar(@cmds)-4);
@cmds_data = (0..scalar(@cmds)-4);
$size = $cmds[1];
$table_name = $cmds[2];
for (my $j = 3 ; $j <= scalar(@cmds) - 1 ; $j++){
print STDERR "\tParser: " . ($j-2) . "º argumento: " . $cmds[$j];
if($cmds[$j] =~ /^name$/){
$cmds_index[$j-3] = 1;
}elsif($cmds[$j] =~ /^lastname$/){
$cmds_index[$j-3] = 2;
}elsif($cmds[$j] =~ /^street$/){
$cmds_index[$j-3] = 3;
}elsif($cmds[$j] =~ /^company$/){
$cmds_index[$j-3] = 4;
}elsif($cmds[$j] =~ /^city$/){
$cmds_index[$j-3] = 5;
}elsif($cmds[$j] =~ /^integer\(\d+;\s?\d+\)$/){
my @aux = split(/^integer\(|\s*;\s*|\)$/, $cmds[$j]);
$cmds_index[$j-3] = 6;
$cmds_bottom[$j-3] = $aux[1];
$cmds_top[$j-3] = $aux[2];
}elsif($cmds[$j] =~ /^char\d+\(\d+;\s?\d+\)$/){
my @aux = split(/^char|\(|\s*;\s*|\)$/, $cmds[$j]); # char26
$cmds_index[$j-3] = 7;
$cmds_bottom[$j-3] = $aux[2];
$cmds_top[$j-3] = $aux[3];
$cmds_data[$j-3] = $aux[1];
}elsif($cmds[$j] =~ /^bool\([1-9]\)$/){
my @aux = split(/^bool\(|\)$/, $cmds[$j]);
$cmds_index[$j-3] = 8;
$cmds_bottom[$j-3] = $aux[1];
}elsif($cmds[$j] =~ /^in\(.+(\|.+)+\)$/){
my @aux = split(/^in\(|\||\)$/, $cmds[$j]);
$cmds_index[$j-3] = 9;
$cmds_data[$j-3] = \@aux;
}elsif($cmds[$j] =~ /^unique_integer\(\d+;\s?\d+\)(\s+AS\s+\w+)?$/){
my @aux = split(/^unique_integer\(|\s*;\s*|\)$/, $cmds[$j]);
$cmds_index[$j-3] = 10;
$cmds_bottom[$j-3] = $aux[1];
$cmds_top[$j-3] = $aux[2];
my @aux_data = &get_unique_integer_random_list($size, $aux[1], $aux[2]);
$cmds_data[$j-3] = \@aux_data;
if($cmds[$j] =~ /^unique_integer\(\d+;\s?\d+\)\s+AS\s+\w+$/){
my @aux_alias = split(/unique_integer\(\d+;\s?\d+\)\s+AS\s+/, $cmds[$j]);
print STDERR ". Alias: " . $aux_alias[1];
if(open(ALIAS, ">>" . $temp_directory . $aux_alias[1] . "." . $time_stamp)){
for(my $o ; $o < $size ; $o++){
print ALIAS $aux_data[$o] . "\n";
}
close(ALIAS);
}else{
print STDERR "\n\tParser: * * * FATAL: imposible crear el alias " . $aux_alias[1] . " * * *";
$errors = $errors+1;
}
}
}elsif($cmds[$j] =~ /^auto_increment\(\d+\)(\s+AS\s+\w+)?$/){
my @aux = split(/^auto_increment\(|\)$/, $cmds[$j]);
$cmds_index[$j-3] = 11;
$cmds_bottom[$j-3] = $aux[1];
if($cmds[$j] =~ /^auto_increment\(\d+\)\s+AS\s+\w+$/){
my @aux_alias = split(/auto_increment\(\d+\)\s+AS\s+/, $cmds[$j]);
print STDERR ". Alias: " . $aux_alias[1];
if(open(ALIAS, ">>" . $temp_directory . $aux_alias[1] . "." . $time_stamp)){
for(my $o ; $o < $size ; $o++){
print ALIAS $aux[1] + $o . "\n";
}
close(ALIAS);
}else{
print STDERR "\n\tParser: * * * FATAL: imposible crear el alias " . $aux_alias[1] . " * * *";
$errors = $errors+1;
}
}
}elsif($cmds[$j] =~ /^unique_in\(\w+\)$/){
$cmds_index[$j-3] = 12;
my @aux = split(/^unique_in\(|\)$/, $cmds[$j]);
if(open(ALIAS, "<" . $temp_directory . $aux[1] . "." . $time_stamp)){
my @unique_in = <ALIAS>;
chomp(@unique_in);
&mix(\@unique_in);
$cmds_data[$j-3] = \@unique_in;
close(ALIAS);
}else{
print STDERR "\n\tParser: * * * FATAL: El alias " . $aux[1] . " no existe * * *";
$errors = $errors+1;
}
}elsif($cmds[$j] =~ /^unique_in_and_drop\(\w+\)$/){
my @aux = split(/^unique_in_and_drop\(|\)$/, $cmds[$j]);
$cmds_index[$j-3] = 13;
if(open(ALIAS, "<" . $temp_directory . $aux[1] . "." . $time_stamp)){
my @unique_in_and_drop = <ALIAS>;
my $unique_in_and_drop_size = scalar(@unique_in_and_drop);
close(ALIAS);
chomp(@unique_in_and_drop);
&mix(\@unique_in_and_drop);
if(open(ALIAS, ">" . $temp_directory . $aux[1] . "." . $time_stamp)){
for(my $o ; $o < $unique_in_and_drop_size - $size ; $o++){
print ALIAS $unique_in_and_drop[$o] . "\n";
}
splice(@unique_in_and_drop, 0, $unique_in_and_drop_size - $size);
$cmds_data[$j-3] = \@unique_in_and_drop;
close(ALIAS);
}else{
print STDERR "\n\tParser: * * * FATAL: imposible acceder al alias " . $aux[1] . " * * *";
$errors = $errors+1;
}
}else{
print STDERR "\n\tParser: * * * FATAL: El alias " . $aux[1] . " no existe * * *";
$errors = $errors+1;
}
}elsif($cmds[$j] =~ /^something_in\(\w+\)$/){
my @aux = split(/^something_in\(|\)$/, $cmds[$j]);
$cmds_index[$j-3] = 14;
if(open(ALIAS, "<" . $temp_directory . $aux[1] . "." . $time_stamp)){
my @something_in = <ALIAS>;
chomp(@something_in);
$cmds_data[$j-3] = \@something_in;
close(ALIAS);
}else{
print STDERR "\n\tParser: * * * FATAL: El alias " . $aux[1] . " no existe * * *";
$errors = $errors+1;
}
}else{
my $aux_data = $cmds[$j];
$cmds_index[$j-3] = 0;
$aux_data =~ s/\\\s\\/ /g;
$cmds_data[$j-3] = $aux_data;
print STDERR " : valor constante: " . $aux_data;
if($cmds[$j] =~ /unique|something|integer|increment|char|\(.+[;|\||,].+\)?/){
print STDERR "\n\tParser: * * * ATENCIÓN: el " . ($j-2) . "º argumento se asemeja a un comando pero se toma como constante * * *";
$warnings = $warnings +1;
}
}
print STDERR "\n";
}
}elsif($args[$i] =~ /^--quote\s+[on|off]/){
print STDERR "\nSomeSqlDump: cambiando comportamiento del comillado:" . $args[$i] . "\n";
$param_quote = (($args[$i] =~ /^--quote\s+on/)?"'":"");
$param_separator = (($args[$i] =~ /^--quote\s+on/)?", ":"");
next;
}elsif($args[$i] =~ /^--parser-only\s+[on|off]/){
print STDERR "\nSomeSqlDump: cambiando comportamiento del parser: " . $args[$i] . "\n";
$parse_only = (($args[$i] =~ /^--parser-only\s+on/)?1:0);
next;
}elsif($args[$i] =~ /^--skip\s+\d+$/){
my @aux = split(/^--skip\s+/, $args[$i]);
print STDERR "Parser: " . $args[$i] . ". Saltando las siguientes " . $aux[1] . " lineas\n";
$i = $i + $aux[1];
next;
}elsif($args[$i] =~ /^--break$/){
print STDERR "Parser: --break. Forzando salida.\n";
last;
}else{
print STDERR "Parser: * * * FATAL: " . $args[$i] . ": no comprendo la sintaxis * * *\n";
$errors = $errors+1;
next;
}
# comienza a generar valores
print STDERR "----------------------------\nSomeSqlDump: creando " . $size . " registros para la tabla " . $table_name . " [ 0% ]";
if(!$parse_only){
print "--\n-- " . $size . " registros para la tabla `" . $table_name . "`\n--\nLOCK TABLES `" . $table_name . "` WRITE;\n/*!40000 ALTER TABLE `" . $table_name . "` DISABLE KEYS */;\n";
for (my $count = 0 ; $count < $size ; $count++){
print "INSERT INTO `" . $table_name . "` VALUES (";
for (my $j = 0 ; $j < scalar(@cmds_index) ; $j++){
if($cmds_index[$j] == 0){
print $cmds_data[$j];
}elsif($cmds_index[$j] == 1){
print $param_quote . &get_random_name() . $param_quote;
}elsif($cmds_index[$j] == 2){
print $param_quote . &get_random_last_name() . $param_quote;
}elsif($cmds_index[$j] == 3){
print $param_quote . &get_random_street() . $param_quote;
}elsif($cmds_index[$j] == 4){
print $param_quote . &get_random_company() . $param_quote;
}elsif($cmds_index[$j] == 5){
print $param_quote . &get_random_city() . $param_quote;
}elsif($cmds_index[$j] == 6){
print &get_random_int($cmds_bottom[$j], $cmds_top[$j]);
}elsif($cmds_index[$j] == 7){
print $param_quote . &get_random_char($cmds_bottom[$j], $cmds_top[$j], $cmds_data[$j]) . $param_quote;
}elsif($cmds_index[$j] == 8){
print ((rand(10) > $cmds_bottom[$j])?"FALSE":"TRUE");
}elsif($cmds_index[$j] == 9){
print ${$cmds_data[$j]}[int(rand(scalar(@{$cmds_data[$j]})-1)+1)];
}elsif($cmds_index[$j] == 10){
print pop(@{$cmds_data[$j]});
}elsif($cmds_index[$j] == 11){
print ($count+$cmds_bottom[$j]);
}elsif($cmds_index[$j] == 12){
print pop(@{$cmds_data[$j]});
}elsif($cmds_index[$j] == 13){
print pop(@{$cmds_data[$j]});
}elsif($cmds_index[$j] == 14){
print ${$cmds_data[$j]}[int(rand(scalar(@{$cmds_data[$j]})))];
}
if($j == scalar(@cmds_index)-1){
print ");\n";
}else{
print $param_separator;
}
}
$global_count = $global_count +1;
if(($count*100)%$size == 0){ print STDERR "\b\b\b\b\b" . (($count/$size < 0.1)?" ":"") . int(($count*100)/$size) . "% ]"; }
}
print "/*!40000 ALTER TABLE `" . $table_name . "` ENABLE KEYS */;\nUNLOCK TABLES;\n\n\n";
}
print STDERR "\b\b\b\b\b100% ]\n----------------------------\n\n";
}
print STDERR "SomeSqlDump: " . $errors . " Errores. " . $warnings . " Advertencias.\n";
print STDERR "SomeSqlDump: " . $global_count . " registros generados en " . (time() - $time_stamp) . " segundos\n";
Ejemplos de uso:
El programa envia el resultado a la salida estandar STDOUT y envia a las salida estandar de error STDERR los mensajes de progreso
Ejemplo en linea de comandos:
$ ./somesqldump.pl 'INSERT 100000 INTO clientes VALUES (auto_increment(1), unique_integer(100;250000), name, lastname);'Ejemplo con archivo de comandos:
$ ./somesqldump.pl < command.sql > datos_dump.sqlEjemplo con archivo de comandos e ingresando la salida directamente al motor de base de datos:
./somesqldump.pl < command.sql | mysql -u root -p base_datosDonde command.sql podria tener algo como:
INSERT 10 INTO tipo_doc VALUES (auto_increment(1) AS id_tipo_doc, char26(4;6));
INSERT 100000 INTO clientes VALUES (auto_increment(1) AS id_cli, something_in(tipo_doc), name, lastname);
INSERT 90000 INTO cliente_comun VALUES (auto_increment(1), unique_in_and_drop(id_cli));
INSERT 10000 INTO cliente_cta_cte VALUES (auto_increment(1), unique_in_and_drop(id_cli));Las cuatro sentencias anteriores cargarán en el archivo datos_dump.sql
- diez registros en la tabla tipo_doc
- cien mil registros para la tabla clientes donde la segunda columna se corresponde con algun valor de la primer colimna de tipo_doc (cumple con una foreign key)
- diez mil registros para la tabla cliente_cta_cte respetando que los valores de la segunda columna se corresponde con algun registro de la primer columna en clientes y no se repite (cumple con una foreign key y unique)
- noventa mil registros para la tabla cliente_comun respetando que los valores de la segunda columna se corresponde con algun registro de la primer columna en clientes, el valor no se repite y ademas no en ninguno de los utilizados anteriormente para la tabla cliente_cta_cte (cumple con una foreign key, unique y exclusion mutua)
Los valores válidos que se pueden ingresar como comando son (están en ingles para que sea mas dificil confundirlos con una constante):
- name : un nombre elejido al azar de una lista
- lastname : un apellido elejido al azar de una lista
- street : un nombre de calle elejido al azar de una lista
- company : un nombre de empresa elejido al azar de una lista
- city un nombre de ciudad elejido al azar de una lista
- integer(min;max) : un entero cualquiera entre min y max
- char26(min;max) : una cadena de letras minusculas de entre min y max caracteres. El nro 26 no es fijo, puede ser cualquier número de dos cifras, si por ejemplo vale 52 incluye tambien letras mayusculas, si es 61 incluye números, si supera el 61 se agregan espacios, un valor 70-75 genera una cadena similar a un texto.
- bool(num) : genera TRUE o FALSE, num indica la probabilidad de TRUE. un 9 genera 90% de valores TRUE
- in(valor_1|valor_2|valor_n) : uno de los valores pasados por parametro, in(TRUE|TRUE|TRUE|TRUE|FALSE) es equivalente a bool(8)
- unique_integer(min;max) [AS alias]: un entero cualquiera entre min y max pero que nunca se repite, se puede crear un alias
- auto_increment(min) [AS alias]: un valor secuencial comenzando desde min, se puede crear un alias
- something_in(alias) : algun valor del alias
- unique_in(alias) : un valor unico dentro de un alias pero que se puede repetir si se invoca mas de una vez el mismo alias
- unique_in_and_drop(alias) : un valor unico dentro de un alias que no se repetirá si se vuelve a invocar el alias (el valor se quita del alias)
Cualquier otro valor se toma como una constante, la cadena / / en una constante será reemplaza por un espacio.
Para unique_in y unique_in_and_drop el alias debe obviamente tener mayor o igual cantidad de elementos que la tabla que se desea crear.
Se cuenta con las siguientes sentencias de control:
- --break : sale ignorando las siquientes sentencias
- --skip num_lineas : salta las siguientes num_lineas sentencias
- --parser-only on : desactiva la generacion de registros, se crean los alias (lo cual resulta útil aveces) pero no se genera salida, se puede reactivar luego con --parser-only off
- --quote off : desactiva las comillas y comas. No se agregan comas para separar los campos ni se coloca comillas simples a las cadenas de caracteres. Se puede reactivar con --quote on
IMPORTANTE: Es muy importante agregar el espacio despues de la coma que separa los comandos
name,street -> constante name,street
name, lastname -> un nombre y una calle al azar separado por comas y entrecomillado
Si se especifica --quote off entonces:
Para agregar una coma como una costante coloque ,,
Para agregar un espacio solo como una constante coloque / /
Para poder utilizar las sentencias name, lastname, etc se debe incluir el archivo somesqldata.pl que ya veré como lo publico (los nombres son en ingles)
Nota: al script lo utilizo desde hace tiempo y funciona bien, estaba originalmente hecho en java, lo traduje a perl ya que está presente en cualquier debian y curiosamente quedó de la mitad del tamaño (350 lineas mas o menos) pero hasta hace menos de un mes nunca habia escrito nada en perl asi que sepan comprender errores e ineficiencias.