package SqlConnector; use strict; ## no critic use threads; use threads::shared; use Globals qw($enablemultithreading); use Logging qw( getlogger dbdebug dbinfo); use LogError qw( dberror dbwarn notimplementederror sortconfigerror); use DBI; use Utils qw(threadid); use Array qw(arrayeq); use RandomString qw(createtmpstring); require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(get_tableidentifier); my $logger = getlogger(__PACKAGE__); my $temptable_randomstringlength = 4; sub new { my $class = shift; my $self = bless {}, $class; my $instanceid = shift; my $cluster = shift; $self->{drh} = undef; $self->{dbh} = undef; $self->{instanceid} = $instanceid; $self->{tid} = threadid(); $self->{sth} = undef; $self->{query} = undef; $self->{params} = undef; $self->{temp_tables} = []; $self->{cluster} = $cluster; return $self; } sub _gettemptablename { my $self = shift; my $temp_tablename = 'TMP_TBL_' . $self->{tid} . '_'; if (length($self->{instanceid}) > 0) { $temp_tablename .= $self->{instanceid} . '_'; } $temp_tablename .= createtmpstring($temptable_randomstringlength); #$self->{temp_table_count}; return $temp_tablename; } sub instanceidentifier { my $self = shift; $self->{instanceid} = shift if @_; return $self->{instanceid}; } sub cluster { my $self = shift; $self->{cluster} = shift if @_; return $self->{cluster}; } sub _connectidentifier { my $self = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return undef; } sub connectidentifier { my $self = shift; my $cluster = $self->{cluster}; if (defined $cluster) { return $cluster->{name}; } else { $self->_connectidentifier(); } } sub tableidentifier { my $self = shift; my $tablename = shift; my (@params) = @_; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return undef; } sub columnidentifier { my $self = shift; my $columnname = shift; my (@params) = @_; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return undef; } sub get_tableidentifier { my ($tablename,@params) = @_; notimplementederror(__PACKAGE__ . ': ' . (caller(0))[3] . ' not implemented',$logger); return undef; } sub getsafetablename { # make a table name (identifier) string save for use within create table statements # of this rdbms connector. my $self = shift; my ($tableidentifier) = @_; #shift; $tableidentifier =~ s/[^0-9a-z_]/_/gi; return $tableidentifier; } sub _extract_indexcols { my $self = shift; my $indexcols = shift; if (defined $indexcols and ref $indexcols eq 'ARRAY') { my @blankcols = map { local $_ = $_; s/\s*\(\d+\).*$//g; $_; } @$indexcols; return \@blankcols; } else { return []; } } sub vacuum { my $self = shift; my $tablename = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); } sub paginate_sort_query { my $self = shift; my $statement = shift; my $offset = shift; my $limit = shift; my $sortingconfigurations = shift; my $orderby = $self->_orderby_columns($sortingconfigurations); if (length($orderby) > 0) { $statement .= ' ORDER BY ' . $orderby; } if (defined $offset and defined $limit) { $statement .= ' LIMIT ' . $offset . ', ' . $limit; } return $statement; } sub _force_numeric_column { my $self = shift; my $column = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); } sub _orderby_columns { my $self = shift; my $sortingconfigurations = shift; my @orderby = (); if (defined $sortingconfigurations) { foreach my $sc (@$sortingconfigurations) { if (defined $sc and ref $sc eq 'HASH') { my $columnname = ((exists $sc->{memberchain}) ? $sc->{memberchain} : $sc->{column}); if (ref $columnname eq 'ARRAY') { $columnname = $columnname->[0]; } if (length($columnname) > 0) { $columnname = $self->columnidentifier($columnname); my $orderby_column; if ($sc->{numeric}) { $orderby_column = $self->_force_numeric_column($columnname); } else { $orderby_column = $columnname; } if (!defined $sc->{dir} or $sc->{dir} > 0) { $orderby_column .= ' ASC'; } else { $orderby_column .= ' DESC'; } push(@orderby,$orderby_column); } else { sortconfigerror(undef,'sort column required',$logger); } } else { sortconfigerror(undef,'invalid sorting configuration',$logger); } } } return join(', ',@orderby); } sub getdatabases { my $self = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return []; } sub _createdatabase { my $self = shift; my ($databasename) = @_; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return []; } sub db_connect { my $self = shift; my (@params) = @_; if (defined $self->{dbh}) { $self->_db_disconnect(); } # child class will do the connect stuff... } sub db_disconnect { my $self = shift; #my $tid = threadid(); my $cluster = $self->{cluster}; if (defined $cluster) { dbdebug($self,'disconnecting database cluster ' . $cluster->{name},$logger); foreach my $node (@{$cluster->{nodes}}) { if ($node->{active}) { my $node_db = &{$node->{get_db}}($self->{instanceid},0); $node_db->_db_disconnect(); } } #$cluster->{scheduling_vars} = {}; } else { $self->_db_disconnect(); } } sub _db_disconnect { my $self = shift; # since this is also called from DESTROY, no die() here! $self->db_finish(); if (defined $self->{dbh}) { #cleartableinfo($self); #dbdebug($self,'disconnecting' . ((defined $self->{cluster}) ? ' ' . $self->_connectidentifier() : ''),$logger); dbdebug($self,'disconnecting',$logger); foreach my $temp_tablename (@{$self->{temp_tables}}) { #if ($self->table_exists($temp_tablename)) { $self->drop_table($temp_tablename); #} } $self->{temp_tables} = []; $self->{dbh}->disconnect() or dbwarn($self,'error disconnecting: ' . $self->{dbh}->errstr(),$logger); $self->{dbh} = undef; dbinfo($self,'disconnected',$logger); #dbinfo($self,((defined $self->{cluster}) ? $self->_connectidentifier() . ' ' : '') . 'disconnected',$logger); } # further disconect code follows in child classes.... } sub is_connected { my $self = shift; return (defined $self->{dbh}); } sub getfieldnames { my $self = shift; my $tablename = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return []; } sub getprimarykeycols { my $self = shift; my $tablename = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return []; } sub create_temptable { my $self = shift; my $select_stmt = shift; my $indexes = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return ''; } sub create_primarykey { my $self = shift; my ($tablename,$keycols,$fieldnames) = @_; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return 0; } sub create_indexes { my $self = shift; my ($tablename,$indexes,$keycols) = @_; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return 0; } sub create_texttable { my $self = shift; my ($tablename,$fieldnames,$keycols,$indexes,$truncate,$defer_indexes) = @_; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return 0; } sub truncate_table { my $self = shift; my $tablename = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); } sub table_exists { my $self = shift; my $tablename = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); return 0; } sub drop_table { my $self = shift; my $tablename = shift; notimplementederror((ref $self) . ': ' . (caller(0))[3] . ' not implemented',$logger); } sub _prepare_error { my $self = shift; my $query = shift; dberror($self,"failed to prepare:\n" . $query . "\nDBI error:\n" . $self->{dbh}->errstr(),$logger); } sub _execute_error { my $self = shift; my $query = shift; my $sth = shift; my $errstr; if (defined $sth) { $errstr = $sth->errstr(); } else { $errstr = $self->{dbh}->errstr(); } dberror($self,"failed to execute:\n" . $query . "\nparameters:\n". join(', ', @_) . "\nDBI error:\n" . $errstr,$logger); } sub _fetch_error { my $self = shift; my $query = shift; my $sth = shift; my $operation = shift; my $index = shift; my $errstr; if (defined $sth) { $errstr = $sth->errstr(); } else { $errstr = $self->{dbh}->errstr(); } dberror($self,'failed with ' . $operation . ":\n" . $query . "\n" . ((defined $index) ? 'column index: ' . $index . "\n" : '') . "parameters:\n". join(', ', @_) . "\nDBI error:\n" . $errstr,$logger); } # This method executes a SQL query that doesn't return any data. The # query may contain placeholders, that will be replaced by the elements # in @params during execute(). The method will die if any error occurs # and return whatever DBI's execute() returned. sub db_do { my $self = shift; my $query = shift; my $result = 0; if (defined $self->{dbh}) { dbdebug($self,'db_do: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $result = $sth->execute(@_) or $self->_execute_error($query,$sth,@_); } return $result; } # get the first value of the first row of data that is returned from the # database. Returns undef if no data is found. sub db_get_value { my $self = shift; my $query = shift; my $row = undef; if (defined $self->{dbh}) { dbdebug($self,'db_get_value: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $sth->execute(@_) or $self->_execute_error($query,$sth,@_); $row = $sth->fetchrow_arrayref(); $self->_fetch_error($query,$sth,'fetchrow_arrayref',undef,@_) if !defined $row and $sth->err(); $sth->finish(); } return ((defined $row) ? $$row[0] : undef); } # get a reference to the first row of data that is returned from the database. # (I.e. whatever is returned by DBI's fetchrow_hashref().) sub db_get_row { my $self = shift; my $query = shift; my $row = []; if (defined $self->{dbh}) { dbdebug($self,'db_get_row: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $sth->execute(@_) or $self->_execute_error($query,$sth,@_); $row = $sth->fetchrow_hashref(); $self->_fetch_error($query,$sth,'fetchrow_hashref',undef,@_) if !defined $row and $sth->err(); $sth->finish(); } return $row; } # get a reference to an array containing the first value of every data row that # is returned from the database like DBI's selectcol_arrayref() does. sub db_get_col { my $self = shift; my $query = shift; my $col = []; if (defined $self->{dbh}) { dbdebug($self,'db_get_col: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $col = $self->{dbh}->selectcol_arrayref($sth, undef, @_); #die "Failed to selectcol_arrayref:\n$query\nDBI error:". $sth->errstr() if !defined $col and $sth->err(); $self->_fetch_error($query,$sth,'selectcol_arrayref',undef,@_) if !defined $col and $sth->err(); $sth->finish(); } return $col; } # get all data that is returned from the database. (I.e. a reference to an # array containing entries returned by DBI's fetchrow_hashref().) sub db_get_all_arrayref { my $self = shift; my $query = shift; my @rows = (); if (defined $self->{dbh}) { dbdebug($self,'db_get_all_arrayref: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $sth->execute(@_) or $self->_execute_error($query,$sth,@_); while (my $row = $sth->fetchrow_hashref()) { $self->_fetch_error($query,$sth,'fetchrow_hashref',undef,@_) if $sth->err(); push @rows, $row; } $sth->finish(); } return \@rows; } # get a reference to a hash containing a hashreference for each row, like DBI's # fetchall_hashref() does. sub db_get_all_hashref { my $self = shift; my $query = shift; my $index = shift; my $result = {}; if (defined $self->{dbh}) { dbdebug($self,'db_get_all_hashref: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $sth->execute(@_) or $self->_execute_error($query,$sth,@_); $result = $sth->fetchall_hashref($index); $self->_fetch_error($query,$sth,'fetchall_hashref',$index,@_) if $sth->err(); $sth->finish(); } return $result; } # get a reference to a hash that is composed of the key_column as keys and the # value_column as values. sub db_get_mapref { my $self = shift; my $query = shift; my $index = shift; my $value = shift; my $result = {}; if (defined $self->{dbh}) { dbdebug($self,'db_get_mapref: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); my $sth = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $sth->execute(@_) or $self->_execute_error($query,$sth,@_); my $rows = $sth->fetchall_hashref($index); #die "Failed to fetchall_hashref:\n$query\nDBI error:". $sth->errstr() if $sth->err(); $self->_fetch_error($query,$sth,'fetchall_hashref',$index,@_) if $sth->err(); foreach my $key (keys %$rows) { $result->{$key} = $$rows{$key}{$value}; } $sth->finish(); return $result; } return $result; } sub db_begin { my $self = shift; if (defined $self->{dbh}) { dbdebug($self,'db_begin',$logger); $self->{dbh}->begin_work() or dberror($self, "failed with begin_transaction \nDBI error:\n" . $self->{dbh}->errstr(),$logger); if ($self->{dbh}->{AutoCommit}) { dbwarn($self,'autocommit was not disabled',$logger); } } } sub db_commit { my $self = shift; if (defined $self->{dbh}) { dbdebug($self,'db_commit',$logger); #komodo workaround: my @wa = $self->{dbh}->commit() or dberror($self, "failed to commit changes\nDBI error:\n" . $self->{dbh}->errstr(),$logger); #remove dberror for debugging } } sub db_rollback { my $self = shift; if (defined $self->{dbh}) { dbdebug($self,'db_rollback',$logger); $self->{dbh}->rollback() or dberror($self, "failed to rollback changes\nDBI error:\n" . $self->{dbh}->errstr(),$logger); } } sub db_quote { my $self = shift; my $value = shift; my $result = $value; if (defined $self->{dbh}) { $result = $self->{dbh}->quote($value) or dberror($self, "failed to quote value\nDBI error:\n" . $self->{dbh}->errstr(),$logger); } return $result; } sub DESTROY { my $self = shift; # perl threads works like a fork, each thread owns a shalow? copy # of the entire current context, at the moment it starts. # due to this, if the thread is finished, perl gc will invoke destructors # on the thread's scope elements, that potentially contains connectors from # the main tread. it will actually attempt destroy them (disconect, etc.) # this is a problem with destructors that change object state like this one # # to avoid this, we perform destruction tasks only if the destructing tid # is the same as the creating one: if ($self->{tid} == threadid()) { $self->_db_disconnect(); delete $self->{drh}; dbdebug($self,(ref $self) . ' connector destroyed',$logger); #} else { # print "NOT destroyed\n"; } } sub lock_tables { my $self = shift; my $tablestolock = shift; $self->db_begin(); } sub unlock_tables { my $self = shift; $self->db_commit(); } sub db_do_begin { my $self = shift; my $query = shift; my $tablename = shift; my $lock = shift; #notimplementederror('db_do_begin',$logger); if (defined $self->{dbh} and not defined $self->{sth} and length($tablename) > 0) { if ($lock) { $self->lock_tables({ $tablename => 'WRITE' }); } dbdebug($self,'db_do_begin: ' . $query,$logger); $self->{sth} = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $self->{query} = $query; $self->{params} = []; } } sub db_do_rowblock { my $self = shift; my $rows = shift; #notimplementederror('db_do_rowblock',$logger); if (defined $self->{dbh} and defined $self->{sth} and defined $rows and ref $rows eq 'ARRAY') { #dberror($self,'test error',$logger); #mysqldbdebug($self,"db_do_rowblock\nrows:\n" . (scalar @$rows),$logger); #mysqldbdebug($self,'db_do_rowblock: ' . $self->{query} . "\nparameters:\n" . join(', ', @_),$logger); foreach my $row (@$rows) { dbdebug($self,'db_do_rowblock: ' . $self->{query} . "\nparameters:\n" . join(', ', @$row),$logger); $self->{sth}->execute(@$row) or $self->_execute_error($self->{query},$self->{sth},@$row); $self->{params} = $row; } } } sub db_get_begin { my $self = shift; my $query = shift; my $tablename = shift; my $lock = shift; if (defined $self->{dbh} and not defined $self->{sth} and length($tablename) > 0) { #eval { $self->lock_tables({ $tablename => 'WRITE' }); }; if ($lock) { $self->lock_tables({ $tablename => 'WRITE' }); } dbdebug($self,'db_get_begin: ' . $query . "\nparameters:\n" . join(', ', @_),$logger); $self->{sth} = $self->{dbh}->prepare($query) or $self->_prepare_error($query); $self->{sth}->execute(@_) or $self->_execute_error($query,$self->{sth},@_); $self->{query} = $query; $self->{params} = \@_; } } sub multithreading_supported { my $self = shift; return 0; } sub db_get_rowblock { my $self = shift; my $max_rows = shift; if ($enablemultithreading) { #my $rows : shared = []; my @rows :shared = (); #my $rows = &share([]); # beware of '&' here!!!! #my $rows = shared_clone({}); if (defined $self->{dbh} and defined $self->{sth}) { dbdebug($self,'db_get_rowblock: ' . $self->{query} . "\nparameters:\n" . join(', ', @{$self->{params}}),$logger); foreach (@{$self->{sth}->fetchall_arrayref(undef, $max_rows)}) { my @row : shared = @{$_}; push @rows, \@row; } $self->_fetch_error($self->{query},$self->{sth},'db_get_rowblock',undef,@{$self->{params}}) if $self->{sth}->err(); } #share(@rows); return \@rows; #return $rows; #return \@rows; } else { my $rows = []; if (defined $self->{dbh} and defined $self->{sth}) { dbdebug($self,'db_get_rowblock: ' . $self->{query} . "\nparameters:\n" . join(', ', @{$self->{params}}),$logger); $rows = $self->{sth}->fetchall_arrayref(undef, $max_rows); $self->_fetch_error($self->{query},$self->{sth},'db_get_rowblock',undef,@{$self->{params}}) if $self->{sth}->err(); } return $rows; } } sub db_finish { my $self = shift; my $unlock = shift; # since this is also called from DESTROY, no die() here! if (defined $self->{dbh} and defined $self->{sth}) { dbdebug($self,'db_finish',$logger); $self->{sth}->finish(); $self->{sth} = undef; if ($unlock) { $self->unlock_tables(); } $self->{query} = undef; $self->{params} = undef; } } 1;