package Local::DB;
=begin # README
================================================================================
wrapper around DBIx::Simple providing overloaded query() and _replace_omniholder()
* dumps query statements to stdout using Term::ANSIColor to colour-code
* logs queries using Local::QueryLogger
* auto-inflates dates (unless switched off in caller)
* allows omniplaceholders (??) in same query as placeholders (?)
usage: my $o = Local::DB->dbix(\%args), then call DBIx::Simple methods on $o
================================================================================
=cut
use parent 'DBIx::Simple';
use Local::DBIx::Simple::Result; # sub-classes DBIx::Simple::Result to auto-inflate dates
use Module::Runtime qw(use_module);
use Data::Printer alias => 'ddp';
use Term::ANSIColor;
use Data::Dumper;
use Modern::Perl;
use Local::Utils;
use Local::QueryLogger;
my $logger = Local::QueryLogger->new();
our %DBIx_Simple_Cache; # so DBIx::Simple object can be shared
sub debug { ddp @_ }
# just returns dsn as array:
sub dsn {
my ($class, $args) = @_;
my @dsn = _mysql_connection_params($args); # warn Dumper \@dsn;
return @dsn;
}
sub query { # inherits from parent, logs query to console and/or log file
my $self = shift; # ddp $self;
my ($str, @bind) = @_; # warn $str; warn Dumper \@bind;
_log_query($str, @bind);
$self->SUPER::query($str, @bind);
}
sub dbix { # provides overloaded query() & _mysql_connection_params()
my ($class, $ref) = @_; # href of username & (db or dsn)
# either db_name or dsn eg (dbi:SQLite:dbname=:memory:)
my $db = $ref->{dsn} || $ref->{dbname} || die 'no dsn or db name';
# this doesn't work for long-running dbix objects in case db disconnects:
# $DBIx_Simple_Cache{$db_name} ||= $class->_new_dbix_simple($args); replaced with:
# returned cached object if dbh still active, or create new & return it:
if ( my $dbix = $DBIx_Simple_Cache{$db} ) {
return $dbix if $dbix->dbh->ping; # only return cached dbix if dbh still active
}
# so either no $DBIx_Simple_Cache{$db_name} or dbh didn't respond to ping so
# create a new one and return it:
return $DBIx_Simple_Cache{$db} = $class->_new_dbix_simple($ref);
=begin # debugging variant of above
my $time_now = Local::Utils::time_now;
my @caller = caller(1); # ddp @caller;
my $caller = $caller[3];
if ( my $dbix = $DBIx_Simple_Cache{$db_name} ) {
my $id = $dbix->dbh->{mysql_thread_id};
# return $dbix if $dbix->dbh->ping;
if ( $dbix->dbh->ping ) { # only return cached dbix if dbh still active:
my $s = "$time_now active dbh (thread_id=$id) so returning cached dbix for $caller";
ddp $s;
return $dbix;
}
my $s = "$time_now have cached dbix for $caller but dbh INACTIVE (thread_id=$id)";
ddp $s;
}
# so either no $DBIx_Simple_Cache{$db_name} or dbh didn't respond to ping so
# create a new one and return it:
my $new_dbix = $class->_new_dbix_simple($args);
my $id = $new_dbix->dbh->{mysql_thread_id};
my $s = "$time_now creating new dbix object for $caller (thread_id=$id)"; ddp $s;
$DBIx_Simple_Cache{$db_name} = $new_dbix;
return $new_dbix;
=cut
}
sub _log_query {
my ($str, @bind) = @_; # debug($str);
# try to emulate output of Rose::DB::Object::QueryBuilder :
{
my $key_words = join '|',
'left join', 'inner join', 'left outer', 'group by', 'order by',
qw(select from where join date_sub curdate interval show columns);
$str =~ s/\b($key_words)\b/uc $1/eg;
$str =~ s/(SELECT)(?!\n)/$1\n /;
# commas (& optional space) - except placeholders (?,?,?,etc):
$str =~ s/(\,(?!\s?\?))(?!\n)/$1\n /g;
$str =~ s/(FROM)(?!\n)/\n$1\n /;
$str =~ s/((INNER|LEFT OUTER)\s?JOIN)(?!\n)/\n\t $1/g;
$str =~ s/(WHERE)(?!\n)/\n$1\n /;
$str =~ s/(ORDER BY)(?!\n)/\n$1\n /;
$str =~ s/(GROUP BY)(?!\n)/\n$1\n /;
$str =~ s/\t/ /g; # tabs -> 2 spaces
}
# my $thread_id = $self->dbh->{mysql_thread_id};
my $divider = '-' x 70;
my $query = do {
no warnings 'uninitialized'; # common for @bind params to be undef
sprintf "[%s] %s\n[%s]", Local::Utils::time_now, $str,
join ', ', # truncate any long @bind elements at 100 chars:
map { length($_) > 103 ? substr($_, 100).'...' : $_ } @bind;
};
my $environment = ( grep $ENV{$_}, qw/RDBO_DEBUG SQL_TRACE/ )
? 'devel' : 'deployment'; warn $environment;
if ( $environment eq 'devel' ) {
print STDERR $divider, "\n";
print STDERR color('yellow');
print STDERR $query, "\n";
print STDERR color('reset'); # ie color('white');
print STDERR $divider, "\n\n";
}
# send to logger
my $log_entry = sprintf "%s\n%s\n", $query, $divider;
$logger->log( $environment, info => $log_entry );
}
sub _new_dbix_simple {
my ($class, $args) = @_;
my @dsn = _mysql_connection_params($args);
my $dbix = $class->SUPER::connect(@dsn); # ddp $dbix; # SUPER is DBIx::Simple
# replace default dbix result_class (DBIx::Simple::Result) with
# Local::DBIx::Simple::Result (provides auto-date-inflation):
$dbix->{result_class} = use_module('Local::DBIx::Simple::Result');
=begin # generates mysql gone away error:
use DBIx::Connector;
my $dbc = DBIx::Connector->new(@dsn); # ddp $dbc;
my $dbix = $class->SUPER::new($dbc->dbh); # ddp $dbix; # SUPER is DBIx::Simple
=cut
=begin # integrated Log4perl dbh:
my $dbh = _log4perl_dbh(@dsn);
my $dbix = $class->SUPER::new($dbh); # ddp $dbix; # SUPER is DBIx::Simple
=cut
return $dbix;
}
=begin # integrated Log4perl dbh:
sub _log4perl_dbh { # uncomment dbix_l4p_logmask in _mysql_connection_params()
use Log::Log4perl;
use DBIx::Log4perl qw(:masks);
Log::Log4perl->init("/path/to/mylog4perl.conf");
return DBIx::Log4perl->connect(@_); # dbh;
}
=cut
sub _mysql_connection_params {
my $args = shift; # href of username & (db or dsn eg dbi:SQLite:dbname=<db>)
# just return dsn if configured (eg SQLite doesn't need username/pwd)
return $args->{dsn} if $args->{dsn};
my $cnf = '/home/raj/.local/mysql.cnf';
-f $cnf || die 'no mysql.cnf file found';
my %attr = (
PrintError => 0,
RaiseError => 1,
mysql_auto_reconnect => 0, # used in conjunction with testing for ping
# uncomment if using log4perl dbh:
# dbix_l4p_logmask => DBIX_L4P_LOG_SQL|DBIX_L4P_LOG_DELAYBINDPARAM,
);
my $dsn = join ';', (
'dbi:mysql:host=localhost',
'database='.$args->{dbname},
'mysql_read_default_file='.$cnf
);
return ( $dsn, undef, undef, \%attr );
}
sub _replace_omniholder {
my ($self, $query, $binds) = @_; # say Dumper [$query, $binds]; # scalarref, arrayref
return if $$query !~ /\(\?\?\)/;
#-------------------------------------------------------------------------------
# count how many individual placeholders in query for subtraction from @$binds:
my $re = qr{\s\?}; # ie individual placeholders - WHERE foo = ? AND bar = ?
# $placeholders_count++ while $$query =~ /$re/g; # warn $placeholders_count;
my $placeholders_count = () = $$query =~ /$re/g; # warn $placeholders_count; # perlfaq4
my $omniholders_count = scalar(@$binds) - $placeholders_count;
#-------------------------------------------------------------------------------
my $omniholders = 0;
#-------------------------------------------------------------------------------
# global vars copied from DBIx::Simple - only used in this sub anyway:
my $quoted = qr/(?:'[^']*'|"[^"]*")*/; # 'foo''bar' simply matches the (?:) twice
my $quoted_mysql = qr/(?:(?:[^\\']*(?:\\.[^\\']*)*)'|"(?:[^\\"]*(?:\\.[^\\"]*)*)")*/;
#-------------------------------------------------------------------------------
my $q = $self->{dbd} =~ /mysql/ ? $quoted_mysql : $quoted; # ddp $q;
$$query =~ s[($q|\(\?\?\))] {
$1 eq '(??)'
? do {
Carp::croak('There can be only one omniholder') if $omniholders++; # ddp $1;
#-------------------------------------------------------------------------------
# '(' . join(', ', ('?') x @$binds) . ')'
'(' . join(', ', ('?') x $omniholders_count) . ')' # adjusted bind count
#-------------------------------------------------------------------------------
}
: $1
}eg; # warn dump $query;
}
1;