######################################################################
## File: $Id: MySQL.pm 10474 2008-01-04 19:09:48Z spadkins $
######################################################################
use App::Repository::DBI;
package App::Repository::MySQL;
$VERSION = (q$Revision: 10474 $ =~ /(\d[\d\.]*)/)[0]; # VERSION numbers generated by svn
@ISA = ( "App::Repository::DBI" );
use strict;
=head1 NAME
App::Repository::MySQL - a MySQL database, accessed through the Repository interface
=head1 SYNOPSIS
use App::Repository::MySQL;
(see man pages for App::Repository and App::Repository::DBI for additional methods)
...
=cut
=head1 DESCRIPTION
The App::Repository::MySQL class encapsulates all access to a MySQL database.
=cut
sub _connect {
&App::sub_entry if ($App::trace);
my $self = shift;
if (!defined $self->{dbh}) {
my $dsn = $self->_dsn();
my $attr = $self->_attr();
while (1) {
eval {
$self->{dbh} = DBI->connect($dsn, $self->{dbuser}, $self->{dbpass}, $attr);
$self->{dbh}{mysql_auto_reconnect} = 1;
};
if ($@) {
delete $self->{dbh};
if ($@ =~ /Lost connection/ || $@ =~ /server has gone away/) {
$self->{context}->log("DBI Exception (retrying) in _connect(): $@");
sleep(1);
}
else {
$self->{context}->log("DBI Exception (fail) in _connect(): $@");
die $@;
}
}
else {
last;
}
}
die "Can't connect to database" if (!$self->{dbh});
}
&App::sub_exit(defined $self->{dbh}) if ($App::trace);
return(defined $self->{dbh});
}
sub _dsn {
&App::sub_entry if ($App::trace);
my ($self) = @_;
my $dbdriver = "mysql";
$self->{dbdriver} = $dbdriver if (!$self->{dbdriver});
my $dsn = $self->{dbdsn};
if (!$dsn) {
my $dbhost = $self->{dbhost};
my $dbport = $self->{dbport};
my $dbsocket = $self->{dbsocket};
my $dbname = $self->{dbname};
my $dbuser = $self->{dbuser};
my $dbpass = $self->{dbpass};
my $dbschema = $self->{dbschema};
my $dbioptions = $self->{dbioptions};
die "ERROR: missing DBI driver and/or db name [$dbdriver,$dbname] in configuration.\n"
if (!$dbdriver || !$dbname);
# NOTE: mysql_client_found_rows=true is important for the following condition.
# If an update is executed against a row that exists, but its values do not change,
# MySQL does not ordinarily report this as a row that has been affected by the
# statement. However, we occasionally need to know if the update found the row.
# We really don't care if the values were changed or not. To get this behavior,
# we need to set this option.
$dsn = "dbi:${dbdriver}:database=${dbname}";
$dsn .= ";host=$dbhost" if ($dbhost);
$dsn .= ";port=$dbport" if ($dbport);
$dsn .= ";mysql_socket=$dbsocket" if ($dbsocket);
$dsn .= ";$dbioptions" if ($dbioptions);
$dsn .= ";mysql_client_found_rows=true";
}
&App::sub_exit($dsn) if ($App::trace);
return($dsn);
}
sub _last_inserted_id {
my ($self, $table) = @_;
return($self->{dbh}{mysql_insertid});
}
sub _mk_select_sql_suffix {
&App::sub_entry if ($App::trace);
my ($self, $table, $options) = @_;
my $suffix = "";
$options = {} if (!$options);
if ($options->{endrow}) {
$suffix = "limit $options->{endrow}\n";
}
&App::sub_exit($suffix) if ($App::trace);
return($suffix);
}
# $insert_sql = $rep->_mk_insert_row_sql ($table, \@cols, \@row);
sub _mk_insert_row_sql {
&App::sub_entry if ($App::trace);
my ($self, $table, $cols, $row, $options) = @_;
$self->_load_table_metadata($table) if (!defined $self->{table}{$table}{loaded});
my $dbh = $self->{dbh};
my ($sql, $values, $col, $value, $colnum, $quoted);
#print $App::DEBUG_FILE "_mk_insert_row_sql($table,\n [",
# join(",",@$cols), "],\n [",
# join(",",@$row), "])\n";
if ($#$cols == -1) {
$self->{error} = "Database->_mk_insert_row_sql(): no columns specified";
return();
}
my $tabcols = $self->{table}{$table}{column};
my $insert = $options->{replace} ? "replace" : "insert";
$sql = "$insert into $table\n";
$values = "values\n";
for ($colnum = 0; $colnum <= $#$cols; $colnum++) {
$col = $cols->[$colnum];
if (!defined $row || $#$row == -1) {
$value = "?";
}
else {
$value = $row->[$colnum];
if (!defined $value) {
$value = "NULL";
}
else {
$quoted = (defined $tabcols->{$col}{quoted}) ? ($tabcols->{$col}{quoted}) : ($value !~ /^-?[0-9.]+$/);
if ($quoted) {
$value = $dbh->quote($value);
}
}
}
$sql .= ($colnum == 0) ? " ($col" : ",\n $col";
if ($tabcols->{$col}{dbexpr_update}) {
$value = sprintf($tabcols->{$col}{dbexpr_update}, $value);
}
$values .= ($colnum == 0) ? " ($value" : ",\n $value";
}
$sql .= ")\n";
$values .= ")\n";
$sql .= $values;
if (!$options->{replace} && $options->{update}) {
my $update = $options->{update};
$sql .= "on duplicate key update";
my $first_update_column = 1;
for ($colnum = 0; $colnum <= $#$cols; $colnum++) {
$col = $cols->[$colnum];
if (!ref($update) || $update->{$col}) {
$sql .= "," if (!$first_update_column);
$first_update_column = 0;
$sql .= "\n $col = values($col)";
}
}
$sql .= "\n";
}
&App::sub_exit($sql) if ($App::trace);
$sql;
}
# $insert_sql = $rep->_mk_insert_rows_sql ($table, \@cols, \@rows, \%options);
# i.e. $options->{replace}
sub _mk_insert_rows_sql {
&App::sub_entry if ($App::trace);
my ($self, $table, $cols, $rows, $options) = @_;
$self->_load_table_metadata($table) if (!defined $self->{table}{$table}{loaded});
my $dbh = $self->{dbh};
if ($#$cols == -1) {
if ($#$rows > -1 && ref($rows->[0]) eq "ARRAY") {
die "_mk_insert_rows_sql(): no columns specified";
}
else {
$cols = [ keys %{$rows->[0]} ];
}
}
my $column_defs = $self->{table}{$table}{column};
my $insert = $options->{replace} ? "replace" : "insert";
my $sql = "$insert into $table\n (" . join(", ", @$cols) . ")\nvalues\n";
my ($value, $colnum, $quoted, $row, $col);
if ($rows && $#$rows > -1 && ref($rows->[0]) eq "ARRAY") {
for (my $rownum = 0; $rownum <= $#$rows; $rownum++) {
$row = $rows->[$rownum];
for ($colnum = 0; $colnum <= $#$cols; $colnum++) {
$col = $cols->[$colnum];
$value = $row->[$colnum];
if (!defined $value) {
$value = "NULL";
}
else {
$quoted = (defined $column_defs->{$col}{quoted}) ? ($column_defs->{$col}{quoted}) : ($value !~ /^-?[0-9.]+$/);
if ($quoted) {
$value = $dbh->quote($value);
}
}
if ($column_defs->{$col}{dbexpr_update}) {
$value = sprintf($column_defs->{$col}{dbexpr_update}, $value);
}
$sql .= ($colnum == 0) ? " ($value" : ", $value";
}
$sql .= ($rownum < $#$rows) ? "),\n" : ")\n";
}
}
else { # if $row is a HASH or OBJECT ...
for (my $rownum = 0; $rownum <= $#$rows; $rownum++) {
$row = $rows->[$rownum];
for ($colnum = 0; $colnum <= $#$cols; $colnum++) {
$col = $cols->[$colnum];
$value = $row->{$col};
if (!defined $value) {
$value = "NULL";
}
else {
$quoted = (defined $column_defs->{$col}{quoted}) ? ($column_defs->{$col}{quoted}) : ($value !~ /^-?[0-9.]+$/);
if ($quoted) {
$value = $dbh->quote($value);
}
}
if ($column_defs->{$col}{dbexpr_update}) {
$value = sprintf($column_defs->{$col}{dbexpr_update}, $value);
}
$sql .= ($colnum == 0) ? " ($value" : ", $value";
}
$sql .= ($rownum < $#$rows) ? "),\n" : ")\n";
}
}
if (!$options->{replace} && $options->{update}) {
my $update = $options->{update};
$sql .= "on duplicate key update";
my $first_update_column = 1;
for ($colnum = 0; $colnum <= $#$cols; $colnum++) {
$col = $cols->[$colnum];
if (!ref($update) || $update->{$col}) {
$sql .= "," if (!$first_update_column);
$first_update_column = 0;
$sql .= "\n $col = values($col)";
}
}
$sql .= "\n";
}
&App::sub_exit($sql) if ($App::trace);
$sql;
}
# $nrows = $rep->_insert_rows ($table, \@cols, \@rows);
sub _insert_rows {
&App::sub_entry if ($App::trace);
my ($self, $table, $cols, $rows, $options) = @_;
$self->{error} = "";
my ($sql, $retval, $nrows_this_insert);
my $dbh = $self->{dbh};
return 0 if (!defined $dbh);
my $nrows = 0;
my $ok = 1;
my $context_options = $self->{context}{options};
my $debug_sql = $context_options->{debug_sql};
my $explain_sql = $context_options->{explain_sql};
my ($timer, $elapsed_time);
if ($debug_sql) {
$timer = $self->_get_timer();
}
my $rows_ref = ref($rows);
if ($rows_ref eq "ARRAY") {
my $maxrows = $options->{maxrows} || 100;
my $rownum = 0;
my (@current_rows, $rownum2);
while ($rownum <= $#$rows) {
$rownum2 = $rownum + $maxrows - 1;
$rownum2 = $#$rows if ($rownum2 > $#$rows);
@current_rows = @{$rows}[($rownum .. $rownum2)];
$nrows_this_insert = $#current_rows + 1;
$sql = $self->_mk_insert_rows_sql($table, $cols, \@current_rows, $options);
if ($debug_sql) {
print $App::DEBUG_FILE "DEBUG_SQL: _insert_rows()\n";
print $App::DEBUG_FILE $sql;
}
### TODO: make this work with regex for retry
$retval = $dbh->do($sql);
if ($debug_sql) {
print $App::DEBUG_FILE "DEBUG_SQL: retval [$retval] $DBI::errstr\n";
print $App::DEBUG_FILE "\n";
}
# The MySQL "insert ... on duplicate key update ..." statement returns 2 rows affected
# when the insert gets a collision and causes an update. So we have to make this
# adjustment. I don't know if it affects the "replace ..." statement in a similar way,
# but I figure this can't hurt.
if ($options->{update} || $options->{replace}) {
if ($retval > $nrows_this_insert) {
$retval = $nrows_this_insert;
}
}
$nrows += $retval;
$rownum += $maxrows;
}
if ($nrows != $#$rows + 1) {
$ok = 0;
}
$self->{numrows} = $nrows;
}
else {
my ($fh);
if (!$rows_ref) {
my $file = $rows; # assume it is a file name
open(App::Repository::MySQL::FILE, $file) || die "Unable to open $file for reading: $!";
$fh = \*App::Repository::MySQL::FILE;
}
else {
$fh = $rows; # assume it is a file handle
}
$rows = []; # we will be refilling this buffer
my %options = ( %$options ); # make a copy so it can be modified
$options{maxrows} = 100;
$nrows = 0;
while (1) {
$rows = $self->_read_rows_from_file($fh, $cols, \%options);
last if ($#$rows == -1);
$sql = $self->_mk_insert_rows_sql($table, $cols, $rows, $options);
if ($debug_sql) {
print $App::DEBUG_FILE "DEBUG_SQL: _insert_rows()\n";
print $App::DEBUG_FILE $sql;
}
### TODO: make this work with regex for retry
$retval = $dbh->do($sql);
if ($debug_sql) {
print $App::DEBUG_FILE "DEBUG_SQL: retval [$retval] $DBI::errstr\n";
print $App::DEBUG_FILE "\n";
}
$nrows += $retval;
if ($retval != $#$rows + 1) {
$ok = 0;
last;
}
}
$self->{numrows} = $nrows;
if (!$rows_ref) {
close(App::Repository::MySQL::FILE);
}
}
if ($debug_sql) {
$elapsed_time = $self->_read_timer($timer);
print $App::DEBUG_FILE "DEBUG_SQL: total rows [$nrows] ($elapsed_time sec)\n";
}
$self->{sql} = $sql;
$self->{numrows} = $nrows;
&App::sub_exit($nrows) if ($App::trace);
return($nrows);
}
###################################################################
# This routine was written because a reliable data load method is
# needed for MySQL 5.1.14+. There are instabilities in this beta
# version of software that cause "load data local infile" and
# extended inserts to both hang the server. Now I am trying to
# write the extended inserts out to a file and load it with the
# "mysql" client program.
###################################################################
# $nrows = $rep->insert_rows_mysql ($table, \@cols, \@rows);
sub insert_rows_mysql {
&App::sub_entry if ($App::trace);
my ($self, $table, $cols, $rows, $options) = @_;
$self->{error} = "";
my ($sql, $retval);
my $dbh = $self->{dbh};
return 0 if (!defined $dbh);
my $nrows = 0;
my $ok = 1;
my $context_options = $self->{context}{options};
my $debug_sql = $context_options->{debug_sql};
my $explain_sql = $context_options->{explain_sql};
my ($timer, $elapsed_time);
if ($debug_sql) {
$timer = $self->_get_timer();
}
my $rows_ref = ref($rows);
if ($rows_ref eq "ARRAY") {
$sql = $self->_mk_insert_rows_sql($table, $cols, $rows, $options);
if ($debug_sql) {
print $App::DEBUG_FILE "DEBUG_SQL: _insert_rows()\n";
print $App::DEBUG_FILE $sql;
}
### TODO: make this work with regex for retry
$retval = $dbh->do($sql);
if ($debug_sql) {
print $App::DEBUG_FILE "DEBUG_SQL: retval [$retval] $DBI::errstr\n";
print $App::DEBUG_FILE "\n";
}
$nrows = $retval;
$self->{numrows} = $nrows;
if ($retval != $#$rows + 1) {
$ok = 0;
}
}
else {
my ($fh, $sqlfh);
my $file = $rows; # $rows must be a file name
open(App::Repository::MySQL::FILE, $file) || die "Unable to open $file for reading: $!";
$fh = \*App::Repository::MySQL::FILE;
open(App::Repository::MySQL::SQL, "| gzip > $file.sql.gz") || die "Unable to open $file.sql.gz for writing: $!";
$sqlfh = \*App::Repository::MySQL::SQL;
$rows = []; # we will be refilling this buffer
my %options = ( %$options ); # make a copy so it can be modified
$options{maxrows} = 100;
$nrows = 0;
while (1) {
$rows = $self->_read_rows_from_file($fh, $cols, \%options);
last if ($#$rows == -1);
$sql = $self->_mk_insert_rows_sql($table, $cols, $rows, $options);
print $sqlfh $sql, ";\n";
$nrows += ($#$rows + 1);
}
if (!$rows_ref) {
close(App::Repository::MySQL::FILE);
close(App::Repository::MySQL::SQL);
}
my $cmd = "zcat $file.sql.gz | mysql --host=$self->{dbhost} --user=$self->{dbuser} --password=$self->{dbpass} $self->{dbname}";
$retval = system($cmd);
if ($retval) {
$nrows = 0;
}
$self->{numrows} = $nrows;
}
$self->{sql} = $sql;
$self->{numrows} = $nrows;
&App::sub_exit($nrows) if ($App::trace);
return($nrows);
}
sub _load_table_key_metadata {
&App::sub_entry if ($App::trace);
my ($self, $table) = @_;
return if (! $table);
my $table_def = $self->{table}{$table};
return if (! $table_def);
my $dbh = $self->{dbh};
# if not defined at all, try to get it from the database
my (@primary_key, @alternate_key, @index, @key, $key_name, $non_unique);
if ($table_def->{phys_table} && (! defined $table_def->{primary_key} || ! defined $table_def->{alternate_key})) {
local $dbh->{FetchHashKeyName} = 'NAME_lc';
my $sth = $dbh->prepare("SHOW INDEX FROM $table");
my $hashes = $dbh->selectall_arrayref($sth, { Columns=>{} });
foreach my $hash (@$hashes) {
if ($key_name && $hash->{key_name} ne $key_name) {
if ($key_name eq 'PRIMARY') {
@primary_key = @key;
}
elsif ($non_unique) {
push(@index, [@key]);
}
else {
push(@alternate_key, [@key]);
}
@key = ();
}
$non_unique = $hash->{non_unique};
$key_name = $hash->{key_name};
push(@key, $hash->{column_name});
}
if ($key_name) {
if ($key_name eq 'PRIMARY') {
@primary_key = @key;
}
elsif ($non_unique) {
push(@index, [@key]);
}
else {
push(@alternate_key, [@key]);
}
}
$table_def->{primary_key} = \@primary_key if (!$table_def->{primary_key});
$table_def->{alternate_key} = \@alternate_key if (!$table_def->{alternate_key} && $#alternate_key > -1);
}
&App::sub_exit() if ($App::trace);
}
# The following patch purportedly adds primary_key() detection directly
# to the DBD where it belongs. Until this is in, I may want to
# duplicate the code in this module.
#diff -ru DBD-mysql-2.9003/lib/DBD/mysql.pm new/lib/DBD/mysql.pm
#--- DBD-mysql-2.9003/lib/DBD/mysql.pm Mon Oct 27 14:26:08 2003
#+++ new/lib/DBD/mysql.pm Tue Mar 2 08:03:17 2004
#@@ -282,7 +282,22 @@
# return map { $_ =~ s/.*\.//; $_ } $dbh->tables();
#}
#-
#+sub primary_key {
#+ my ($dbh, $catalog, $schema, $table) = @_;
#+ my $table_id = $dbh->quote_identifier($catalog, $schema, $table);
#+ local $dbh->{FetchHashKeyName} = 'NAME_lc';
#+ my $desc_sth = $dbh->prepare("SHOW INDEX FROM $table_id");
#+ my $desc = $dbh->selectall_arrayref($desc_sth, { Columns=>{} });
#+ my %keys;
#+ foreach my $row (@$desc) {
#+ if ($row->{key_name} eq 'PRIMARY') {
#+ $keys{$row->{column_name}} = $row->{seq_in_index};
#+ }
#+ }
#+ my (@keys) = sort { $keys{$a} <=> $keys{$b} } keys %keys;
#+ return (@keys);
#+}
#+
#sub column_info {
# my ($dbh, $catalog, $schema, $table, $column) = @_;
# return $dbh->set_err(1, "column_info doesn't support table wildcard")
#############################################################################
# METHODS
#############################################################################
=head1 Methods: Import/Export Data From File
=cut
#############################################################################
# import_rows()
#############################################################################
=head2 import_rows()
* Signature: $rep->import_rows($table, $file);
* Signature: $rep->import_rows($table, $file, $options);
* Param: $table string
* Param: $file string
* Param: $options named
* Param: columns ARRAY names of columns of the fields in the file
* Param: import_method string [basic=invokes generic superclass to do work,
insert=loads with multiple-row inserts,
<otherwise>=use "load data infile"]
* Param: local boolean file is on client machine rather than database server
* Param: replace boolean rows should replace existing rows based on unique indexes
* Param: field_sep char character which separates the fields in the file (can by "\t")
* Param: field_quote char character which optionally encloses the fields in the file (i.e. '"')
* Param: field_escape char character which escapes the quote chars within quotes (i.e. "\")
* Return: void
* Throws: App::Exception::Repository
* Since: 0.01
Note: If you want to call this with $options->{local}, you will probably
need to make sure that mysql_local_infile=1 is in your DSN. This might
require a line like the following in your "app.conf" file.
dbioptions = mysql_local_infile=1
Sample Usage:
$rep->import_rows("usr","usr.dat");
# root:x:0:0:root:/root:/bin/bash
$rep->import_rows("usr", "/etc/passwd" ,{
field_sep => ":",
columns => [ "username", "password", "uid", "gid", "comment", "home_directory", "shell" ],
});
=cut
#SYNTAX:
#LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name.txt'
# [REPLACE | IGNORE]
# INTO TABLE tbl_name
# [FIELDS
# [TERMINATED BY 'string']
# [[OPTIONALLY] ENCLOSED BY 'char']
# [ESCAPED BY 'char' ]
# ]
# [LINES
# [STARTING BY 'string']
# [TERMINATED BY 'string']
# ]
# [IGNORE number LINES]
# [(col_name_or_user_var,...)]
# [SET col_name = expr,...)]
sub import_rows {
&App::sub_entry if ($App::trace);
my ($self, $table, $columns, $file, $options) = @_;
$columns = $self->_get_default_columns($table) if (!$columns);
my $nrows = 0;
my $import_method = $options->{import_method} || $self->{import_method} || "";
if ($import_method eq "basic") {
$nrows = $self->SUPER::import_rows($table, $columns, $file, $options);
}
elsif ($import_method eq "insert") {
$nrows = $self->insert_rows($table, $columns, $file, $options);
}
elsif ($import_method eq "insert_mysql") {
$nrows = $self->insert_rows_mysql($table, $columns, $file, $options);
}
else {
my $local = $options->{local};
$local = 1 if (!defined $local);
my $local_modifier = $local ? " local" : "";
my $sql = "load data$local_modifier infile '$file' into table $table";
if ($options->{field_sep} || $options->{field_quote} || $options->{field_escape}) {
$sql .= "\nfields";
$sql .= "\n terminated by '$options->{field_sep}'" if ($options->{field_sep});
$sql .= "\n optionally enclosed by '$options->{field_quote}'" if ($options->{field_quote});
$sql .= "\n escaped by '$options->{field_escape}'" if ($options->{field_escape});
}
$sql .= "\n(" . join(",", @$columns) . ")\n";
my $context_options = $self->{context}{options};
my $debug_sql = $context_options->{debug_sql};
my ($timer, $elapsed_time);
if ($debug_sql) {
$timer = $self->_get_timer();
print $App::DEBUG_FILE "DEBUG_SQL: import_rows()\n";
print $App::DEBUG_FILE $sql;
}
eval {
$nrows = $self->{dbh}->do($sql);
};
if ($debug_sql) {
$elapsed_time = $self->_read_timer($timer);
print $App::DEBUG_FILE "DEBUG_SQL: import_rows=[$nrows] ($elapsed_time sec) $DBI::errstr : $@\n";
}
die $@ if ($@);
}
&App::sub_exit($nrows) if ($App::trace);
return($nrows);
}
#############################################################################
# export_rows()
#############################################################################
=head2 export_rows()
* Signature: $rep->export_rows($table, $file);
* Signature: $rep->export_rows($table, $file, $options);
* Param: $table string
* Param: $file string
* Param: $options named
* Param: columns ARRAY names of columns of the fields in the file
* Param: export_method string [basic=invokes generic superclass to do work]
* Param: field_sep char character which separates the fields in the file (can by "\t")
* Param: field_quote char character which optionally encloses the fields in the file (i.e. '"')
* Param: field_escape char character which escapes the quote chars within quotes (i.e. "\")
* Return: void
* Throws: App::Exception::Repository
* Since: 0.01
Sample Usage:
$rep->export_rows("usr","usr.dat");
# root:x:0:0:root:/root:/bin/bash
$rep->export_rows("usr", "passwd.dat" ,{
field_sep => ":",
columns => [ "username", "password", "uid", "gid", "comment", "home_directory", "shell" ],
});
=cut
#SELECT ... INTO OUTFILE is the complement of LOAD DATA INFILE; the syntax for the
#export_options part of the statement consists of the same FIELDS and LINES clauses
#that are used with the LOAD DATA INFILE statement.
#See Section 13.2.5, .LOAD DATA INFILE Syntax..
#SELECT
# [ALL | DISTINCT | DISTINCTROW ]
# [HIGH_PRIORITY]
# [STRAIGHT_JOIN]
# [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
# [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
# select_expr, ...
# [INTO OUTFILE 'file_name' export_options
# | INTO DUMPFILE 'file_name']
# [FROM table_references
# [WHERE where_definition]
# [GROUP BY {col_name | expr | position}
# [ASC | DESC], ... [WITH ROLLUP]]
# [HAVING where_definition]
# [ORDER BY {col_name | expr | position}
# [ASC | DESC] , ...]
# [LIMIT {[offset,] row_count | row_count OFFSET offset}]
# [PROCEDURE procedure_name(argument_list)]
# [FOR UPDATE | LOCK IN SHARE MODE]]
sub export_rows {
&App::sub_entry if ($App::trace);
my ($self, $table, $params, $file, $options) = @_;
if ($options->{export_method} && $options->{export_method} eq "basic") {
$self->SUPER::export_rows($table, $file, $options);
}
else {
my $columns = $options->{columns} || $self->{table}{$table}{columns};
my $where_clause = $self->_mk_where_clause($table, $params, $options);
my $sql = "select\n " . join(",\n ", @$columns);
$sql .= "\n$where_clause" if ($where_clause);
$sql .= "\ninto outfile '$file'";
if ($options->{field_sep} || $options->{field_quote} || $options->{field_escape}) {
$sql .= "\nfields";
$sql .= "\n terminated by '$options->{field_sep}'" if ($options->{field_sep});
$sql .= "\n optionally enclosed by '$options->{field_quote}'" if ($options->{field_quote});
$sql .= "\n escaped by '$options->{field_escape}'" if ($options->{field_escape});
}
$sql .= "\n";
my $context_options = $self->{context}{options};
my $debug_sql = $context_options->{debug_sql};
my ($timer, $elapsed_time);
if ($debug_sql) {
$timer = $self->_get_timer();
print $App::DEBUG_FILE "DEBUG_SQL: export_rows()\n";
print $App::DEBUG_FILE $sql;
}
my ($retval);
eval {
$retval = $self->{dbh}->do($sql);
};
if ($debug_sql) {
$elapsed_time = $self->_read_timer($timer);
print $App::DEBUG_FILE "DEBUG_SQL: export_rows=[$retval] ($elapsed_time sec) $DBI::errstr : $@\n";
}
}
&App::sub_exit() if ($App::trace);
}
#+----+-------------+-------+-------+-------------------------------------+-------------------+---------+-------------+------+-------+
#| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
#+----+-------------+-------+-------+-------------------------------------+-------------------+---------+-------------+------+-------+
#| 1 | SIMPLE | t1 | const | hotel_prop_ds_ak1,hotel_prop_ds_ie1 | hotel_prop_ds_ak1 | 9 | const,const | 1 | |
#+----+-------------+-------+-------+-------------------------------------+-------------------+---------+-------------+------+-------+
sub explain_sql {
my ($self, $sql) = @_;
my $dbh = $self->{dbh};
# NOTE: MySQL "explain" only works for "select".
# We convert "update" and "delete" to "select" to explain them.
if (defined $dbh) {
if ($sql =~ s/^delete/select */is) {
# do nothing
}
elsif ($sql =~ s/^update\s+(.*)\sset\s+.*\swhere/select * from $1\nwhere/is) {
# do nothing
}
if ($sql =~ /^select/i) {
my ($rows, $posskeys, $key, $keylen);
eval {
$rows = $dbh->selectall_arrayref("explain $sql");
};
print $App::DEBUG_FILE "EXPLAIN_SQL: $DBI::errstr\n";
if ($rows) {
print $App::DEBUG_FILE "+----+-------------+----------------------+-------+----------------------+---------+----------+\n";
print $App::DEBUG_FILE "| id | select_type | table | type | key | key_len | rows |\n";
print $App::DEBUG_FILE "+----+-------------+----------------------+-------+----------------------+---------+----------+\n";
foreach my $row (@$rows) {
$key = $row->[5];
$keylen = length($key);
if ($keylen > 21) {
$key = substr($key,0,12) . ".." . substr($key,$keylen-7,7);
}
printf($App::DEBUG_FILE "|%3s | %-12s| %-21s| %-6s| %-21s|%8d |%9d | %s\n", @{$row}[0,1,2,3], $key, @{$row}[6,8]);
}
print $App::DEBUG_FILE "+----+----------------------------------------------------------------------------------------+\n";
print $App::DEBUG_FILE "| id | possible_keys/ref/extra\n";
print $App::DEBUG_FILE "+----+----------------------------------------------------------------------------------------+\n";
foreach my $row (@$rows) {
$key = $row->[5];
$posskeys = $row->[4];
$posskeys =~ s/\b($key)\b/[$key]/;
printf($App::DEBUG_FILE "|%3s | posskeys: %s\n", $row->[0], $posskeys);
printf($App::DEBUG_FILE "|%3s | ref: %s; extra: %s\n", @{$row}[0,7,9]);
}
print $App::DEBUG_FILE "+---------------------------------------------------------------------------------------------+\n";
}
}
else {
$sql =~ /^\s*(\S*)/;
print $App::DEBUG_FILE "EXPLAIN_SQL: Can't explain $1 statement.\n";
}
}
}
1;