17 Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
18 Copyright [2016-2024] EMBL-European Bioinformatics Institute
20 Licensed under the Apache License,
Version 2.0 (the
"License"); you may not use
this file except in compliance with the License.
21 You may obtain a copy of the License at
25 Unless required by applicable law or agreed to in writing, software distributed under the License
26 is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27 See the License
for the specific language governing permissions and limitations under the License.
31 Please subscribe to the
Hive mailing list: http:
36 package Bio::EnsEMBL::Hive::DBSQL::DBConnection;
41 use Time::HiRes (
'usleep');
46 use base (
'Bio::EnsEMBL::Hive::DBSQL::CoreDBConnection');
53 if(my $url =
delete $flags{
'-url'}) {
56 foreach my $name (
'driver',
'host',
'port',
'user',
'pass',
'dbname' ) {
59 foreach my $name ( keys %{$parsed_url->{
'conn_params'}} ) {
63 return $class->SUPER::new( %flags );
66 throw(
"Could not create DBC because could not parse the URL '$url'");
69 return $class->SUPER::new( @_ );
74 sub _optional_pair { # helper
function
75 my ($key, $value) = @_;
77 return defined($value) ? ($key => $value) : ();
82 my ($self, $psw_env_var_name) = @_;
85 if($psw_expression = $self->password) {
86 if($psw_env_var_name) {
87 $ENV{$psw_env_var_name} = $psw_expression;
88 $psw_expression =
'${'.$psw_env_var_name.
'}';
93 _optional_pair(
'driver', $self->driver),
94 _optional_pair(
'user', $self->username),
95 _optional_pair(
'pass', $psw_expression),
96 _optional_pair(
'host', $self->host),
97 _optional_pair(
'port', $self->port),
98 _optional_pair(
'dbname', $self->dbname),
101 _optional_pair(
'disconnect_when_inactive', $self->disconnect_when_inactive),
102 _optional_pair(
'wait_timeout', $self->wait_timeout),
103 _optional_pair(
'reconnect_when_lost', $self->reconnect_when_lost),
113 Arg [1] : String $environment_variable_name_to_store_password_in (optional)
114 Example : $url = $dbc->url;
115 Description: Constructs a URL
string for this database connection.
116 Returntype :
string of format mysql:
124 my ($self, $psw_env_var_name) = @_;
130 sub connect { # a wrapper that imitates CSMA/CD protocol
's incremental backoff-and-retry approach
137 foreach my $attempt (1..$attempts) {
139 $retval = $self->SUPER::connect( @_ );
142 if (Bio::EnsEMBL::Hive::Utils::SQLErrorParser::is_server_too_busy($self->driver, $@)) {
144 warn "Possibly transient problem conecting to the database (attempt #$attempt). Will try again in $sleep_sec sec";
146 usleep( $sleep_sec*1000000 );
150 } else { # but definitely report other errors
155 last; # stop looping once we succeeded
159 die "After $attempts attempts still could not connect() : $@";
166 sub protected_prepare_execute { # try to resolve certain mysql "Deadlocks" by imitating CSMA/CD protocol's incremental backoff-and-retry approach (a useful workaround even in mysql 5.1.61)
168 my $sql_params = shift @_;
169 my $deadlock_log_callback = shift @_;
171 my $sql_cmd = shift @$sql_params;
174 my $sleep_max_sec = 1;
179 foreach my $attempt (1..$attempts) {
181 my $sth = $self->prepare( $sql_cmd );
182 $retval = $sth->execute( @$sql_params );
186 $query_msg =
"QUERY: $sql_cmd, PARAMS: (".join(
', ',@$sql_params).
")";
190 my $this_sleep_sec = int( rand( $sleep_max_sec )*100 ) / 100.0;
192 if( $deadlock_log_callback ) {
193 $deadlock_log_callback->(
" temporarily failed due to a DEADLOCK in the database (attempt #$attempt). Will try again in $this_sleep_sec sec" );
196 usleep( $this_sleep_sec*1000000 );
200 }
else { # but definitely report other errors
202 die
"$@ -- $query_msg";
205 last; # stop looping once we succeeded
208 die
"After $attempts attempts the query $query_msg still cannot be run: $@" if($@);
214 our $pass_internal_counter = 0;
216 my ($self, $executable, $prepend, $append, $sqlcmd, $hide_password_in_env) = @_;
218 my $driver = $self->driver ||
'mysql';
220 my $dbname = $self->dbname;
222 if($sqlcmd =~ /(DROP\s+DATABASE(?:\s+IF\s+EXISTS)?\s*?)(?:\s+(\w+))?/i) {
225 if($driver eq
'sqlite') {
226 return [
'rm',
'-f', $dbname];
229 die
"'DROP DATABASE' needs a database name\n";
231 if ($driver eq
'mysql') {
232 $sqlcmd =
"$1 \`$dbname\`" unless $2;
234 $sqlcmd =
"$1 $dbname" unless $2;
238 } elsif($sqlcmd =~ /(CREATE\s+DATABASE(?:\s+IF\s+NOT\s+EXISTS)?\s*?)(?:\s+(\w+))?/i ) {
241 if($driver eq
'sqlite') {
242 return [
'touch', $dbname];
245 die
"'CREATE DATABASE' needs a database name\n";
247 my %limits = (
'mysql' => 64,
'pgsql' => 63 );
248 if (length($dbname) > $limits{$driver}) {
249 die
"Database name '$dbname' is too long (> $limits{$driver}). Cannot create the database\n";
251 if ($driver eq
'mysql') {
252 $sqlcmd =
"$1 \`$dbname\`" unless $2;
254 $sqlcmd =
"$1 $dbname" unless $2;
264 if ($self->password) {
265 if ($hide_password_in_env) {
266 my $pass_variable =
"EHIVE_TMP_PASSWORD_${pass_internal_counter}";
267 $pass_internal_counter++;
268 $ENV{$pass_variable} = $self->password;
269 $hidden_password =
'$'.$pass_variable;
271 $hidden_password = $self->password;
275 if($driver eq
'mysql') {
276 $executable ||=
'mysql';
278 push @cmd, (
'env',
'MYSQL_PWD='.$hidden_password)
if ($self->password);
279 push @cmd, $executable;
280 push @cmd, @$prepend
if ($prepend && @$prepend);
281 push @cmd,
'--host='.$self->host
if $self->host;
282 push @cmd,
'--port='.$self->port
if $self->port;
283 push @cmd,
'--user='.$self->username
if $self->username;
284 # push @cmd, '--password='.$hidden_password if $self->password;
285 push @cmd, (
'-e', $sqlcmd)
if $sqlcmd;
286 push @cmd, $dbname
if $dbname;
288 } elsif($driver eq
'pgsql') {
289 $executable ||=
'psql';
291 push @cmd, (
'env',
'PGPASSWORD='.$hidden_password)
if ($self->password);
292 push @cmd, $executable;
293 push @cmd, @$prepend
if ($prepend && @$prepend);
294 push @cmd, (
'-h', $self->host)
if defined($self->host);
295 push @cmd, (
'-p', $self->port)
if defined($self->port);
296 push @cmd, (
'-U', $self->username)
if defined($self->username);
297 push @cmd, (
'-c', $sqlcmd)
if $sqlcmd;
298 push @cmd, $dbname
if $dbname;
300 } elsif($driver eq
'sqlite') {
301 $executable ||=
'sqlite3';
303 die
"sqlite requires a database (file) name\n" unless $dbname;
305 push @cmd, $executable;
306 push @cmd, @$prepend
if ($prepend && @$prepend);
308 push @cmd, $sqlcmd
if $sqlcmd;
311 push @cmd, @$append
if ($append && @$append);
317 =head2 run_in_transaction
319 Description : Wrapper that first sets AutoCommit to 0, runs some user code, and at the end issues a commit() / rollback()
320 It also has to temporarily set disconnect_when_inactive() to 1 because a value of 0 would cause the
321 DBConnection
object to disconnect early, which would rollback the transaction.
322 NB: This is essentially a trimmed copy of Ensembl's Utils::SqlHelper::transaction()
326 sub run_in_transaction {
327 my ($self, $callback) = @_;
329 # Save the original value of disconnect_when_inactive()
330 my $original_dwi = $self->disconnect_when_inactive();
331 $self->disconnect_when_inactive(0);
333 $self->reconnect() unless $self->db_handle()->ping();
335 # Save the original value of "AutoCommit"
336 my $original_ac = $self->db_handle()->{
'AutoCommit'};
337 $self->db_handle()->{
'AutoCommit'} = 0;
341 $result = $callback->();
342 # FIXME: does this work if the "MySQL server has gone away" ?
343 $self->db_handle()->commit();
347 #If there is an error then we apply rollbacks
349 eval { $self->db_handle()->rollback(); };
352 # Restore the original values
353 $self->db_handle()->{
'AutoCommit'} = $original_ac;
354 $self->disconnect_when_inactive($original_dwi);
356 die
"ABORT: Transaction aborted because of error: ${error}" if $error;
361 =head2 has_write_access
363 Example : my $can_do = $dbc->has_write_access();
364 Description : Tells whether the underlying database connection has write access to the database
372 sub has_write_access {
374 if ($self->driver eq
'mysql') {
375 my $current_user = $self->selectrow_arrayref(
'SELECT CURRENT_USER()');
376 # munge grantee - user and host specification need single quoting
377 my $grantee = join
'@',
map { qq{
'$_'} } split /@/, @$current_user[0];
378 # instance wide privileges
381 FROM information_schema.user_privileges
382 WHERE PRIVILEGE_TYPE IN ('INSERT', 'DELETE', 'UPDATE')
385 $self->selectall_arrayref($access_sql, undef, $grantee);
386 # schema specific privileges
389 FROM information_schema.schema_privileges
390 WHERE PRIVILEGE_TYPE IN ('INSERT', 'DELETE', 'UPDATE')
391 AND DATABASE() LIKE TABLE_SCHEMA
394 $self->selectall_arrayref($access_sql, undef, $grantee);
395 my $has_write_access_from_some_host = 0;
396 foreach my $entry (@$user_entries, @$schema_entries) {
397 $has_write_access_from_some_host ||= !!(3 == @$entry[0]);
399 return $has_write_access_from_some_host;
400 } elsif ($self->driver eq
'pgsql') {
403 FROM (SELECT DISTINCT PRIVILEGE_TYPE
404 FROM information_schema.table_privileges
405 WHERE PRIVILEGE_TYPE IN ('INSERT', 'DELETE', 'UPDATE')
406 AND GRANTEE = current_user
407 AND TABLE_CATALOG = current_database()) AS temp};
409 $self->selectall_arrayref($access_sql, undef);
410 my $has_write_access_from_some_host = 0;
411 foreach my $entry (@$user_entries) {
412 $has_write_access_from_some_host ||= !!(3 == @$entry[0]);
414 return $has_write_access_from_some_host;
416 # TODO: implement this for other drivers
421 =head2 requires_write_access
423 Example : $dbc->requires_write_access();
424 Description : See Exceptions
426 Exceptions : Throws
if the current user hasn
't write access to the database
432 sub requires_write_access {
434 unless ($self->has_write_access) {
435 die sprintf("It appears that %s doesn't have INSERT/UPDATE/DELETE privileges on
this database (%s). Please check the credentials\n
", $self->username, $self->dbname);
440 =head2 _interval_seconds_sql
442 Argument[1] : String. Name of the column for the start of the interval
443 Argument[2] : String, optional. Name of the column for the end of the interval
444 Example : $self->dbc->_interval_seconds_sql();
445 Description : Returns an SQL expression to compute the number of seconds betwen both columns.
446 If the second column name is missing, compute the number of seconds until now instead.
454 sub _interval_seconds_sql {
455 my ($self, $column_from, $column_to) = @_;
457 my $driver = $self->driver();
461 'sqlite' => q{'now'},
462 'pgsql' => 'CURRENT_TIMESTAMP',
466 'mysql' => "UNIX_TIMESTAMP($column_to)-UNIX_TIMESTAMP($column_from)
",
467 'sqlite' => "strftime(
'%s',$column_to)-strftime(
'%s',$column_from)
",
468 'pgsql' => "EXTRACT(EPOCH FROM $column_to - $column_from)
",