diff options
Diffstat (limited to 'Bugzilla/DB.pm')
-rw-r--r-- | Bugzilla/DB.pm | 2040 |
1 files changed, 1123 insertions, 917 deletions
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 5bc83f9d6..abed6ea0c 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -26,14 +26,19 @@ use Bugzilla::Error; use Bugzilla::DB::Schema; use Bugzilla::Version; +## REDHAT EXTENSION 987269 +use File::Basename; + use List::Util qw(max); use Storable qw(dclone); +use Text::ParseWords qw(shellwords); +use Data::Dumper; ##################################################################### # Constants ##################################################################### -use constant BLOB_TYPE => DBI::SQL_BLOB; +use constant BLOB_TYPE => DBI::SQL_BLOB; use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Set default values for what used to be the enum types. These values @@ -46,14 +51,14 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Bugzilla with enums. After that, they are either controlled through # the Bugzilla UI or through the DB. use constant ENUM_DEFAULTS => { - bug_severity => ['blocker', 'critical', 'major', 'normal', - 'minor', 'trivial', 'enhancement'], - priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], - op_sys => ["All","Windows","Mac OS","Linux","Other"], - rep_platform => ["All","PC","Macintosh","Other"], - bug_status => ["UNCONFIRMED","CONFIRMED","IN_PROGRESS","RESOLVED", - "VERIFIED"], - resolution => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"], + bug_severity => + ['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', 'enhancement'], + priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], + op_sys => ["All", "Windows", "Mac OS", "Linux", "Other"], + rep_platform => ["All", "PC", "Macintosh", "Other"], + bug_status => + ["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"], + resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"], }; # The character that means "OR" in a boolean fulltext search. If empty, @@ -83,14 +88,14 @@ use constant WORD_END => '($|[^[:alnum:]])'; use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1; ##################################################################### -# Overridden Superclass Methods +# Overridden Superclass Methods ##################################################################### sub quote { - my $self = shift; - my $retval = $self->SUPER::quote(@_); - trick_taint($retval) if defined $retval; - return $retval; + my $self = shift; + my $retval = $self->SUPER::quote(@_); + trick_taint($retval) if defined $retval; + return $retval; } ##################################################################### @@ -98,95 +103,172 @@ sub quote { ##################################################################### sub connect_shadow { - my $params = Bugzilla->params; - die "Tried to connect to non-existent shadowdb" - unless $params->{'shadowdb'}; + my $params = Bugzilla->params; + die "Tried to connect to non-existent shadowdb" unless $params->{'shadowdb'}; - # Instead of just passing in a new hashref, we locally modify the - # values of "localconfig", because some drivers access it while - # connecting. - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_host} = $params->{'shadowdbhost'}; - $connect_params{db_name} = $params->{'shadowdb'}; - $connect_params{db_port} = $params->{'shadowdbport'}; - $connect_params{db_sock} = $params->{'shadowdbsock'}; + # Instead of just passing in a new hashref, we locally modify the + # values of "localconfig", because some drivers access it while + # connecting. + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_host} = $params->{'shadowdbhost'}; + $connect_params{db_name} = $params->{'shadowdb'}; + $connect_params{db_port} = $params->{'shadowdbport'}; + $connect_params{db_sock} = $params->{'shadowdbsock'}; - return _connect(\%connect_params); + return _connect(\%connect_params); } sub connect_main { - my $lc = Bugzilla->localconfig; - return _connect(Bugzilla->localconfig); + my $lc = Bugzilla->localconfig; + return _connect(Bugzilla->localconfig); } sub _connect { - my ($params) = @_; + my ($params) = @_; + + my $driver = $params->{db_driver}; + my $pkg_module = DB_MODULE->{lc($driver)}->{db}; - my $driver = $params->{db_driver}; - my $pkg_module = DB_MODULE->{lc($driver)}->{db}; + # do the actual import + eval("require $pkg_module") + || die( + "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@); - # do the actual import - eval ("require $pkg_module") - || die ("'$driver' is not a valid choice for \$db_driver in " - . " localconfig: " . $@); + # instantiate the correct DB specific module + my $dbh = $pkg_module->new($params); - # instantiate the correct DB specific module - my $dbh = $pkg_module->new($params); +## BUGBUG uncomment this to get profiling of the SQL +## $dbh->{Profile} = "!Statement:!MethodName:!MethodClass:!Caller2/DBI::ProfileDumper::Apache/Dir:data"; - return $dbh; + return $dbh; } sub _handle_error { - require Carp; - - # Cut down the error string to a reasonable size - $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) - if length($_[0]) > 4000; - $_[0] = Carp::longmess($_[0]); - return 0; # Now let DBI handle raising the error + require Carp; + +# Check for some common error cases in postgres database and pop up a nicer error message + if (!Bugzilla->request_cache->{in_error} + && lc(Bugzilla->localconfig->{db_driver}) eq 'pg') + { + ## REDHAT EXTENSION BEGIN 1174110 + # If a serialization error happened, tell the user to try again. + if ($_[0] =~ /could not serialize access due to concurrent update/) { + Bugzilla->request_cache->{in_error} = 1; + mail_db_error( + 'Serialization Error', + $_[0], + (Carp::longmess((Bugzilla->dbh->{Statement} || "No query statement found"),)), + Dumper($_[1]->{ParamValues}) + ); + + my $error_msg + = "<pre>\n" + . Carp::longmess((Bugzilla->dbh->{Statement} || "No query statement found")) + . "\n</pre>"; + if (i_am_cgi()) { + ## REDHAT EXTENSION 1276162 + ThrowUserError("db_sync_error_web", {query => $error_msg}); + } + else { # Cover RPC and console use cases + ThrowUserError("db_sync_error_other", {query => $error_msg}); + } + } + ## REDHAT EXTENSION END 1174110 + ## REDHAT EXTENSION BEGIN 1228512 + if ($_[0] =~ /invalid input syntax for type timestamp: (".*")\n/) { + Bugzilla->request_cache->{in_error} = 1; + ThrowUserError("db_invalid_timestamp_format", {timestamp => $1}); + } + ## REDHAT EXTENSION END 1228512 + ## REDHAT EXTENSION BEGIN 1250304 + if ($_[0] =~ /canceling statement due to statement timeout/) { + Bugzilla->request_cache->{in_error} = 1; + ## REDHAT EXTENSION START 1561831 + # Long queries can timeout the connection + Bugzilla->check_dbh(); + ## REDHAT EXTENSION START 1561831 + ThrowUserError("query_timeout", + {timeout => Bugzilla->params->{'long_query_timeout'}}); + } + ## REDHAT EXTENSION END 1250304 + ## RED HAT EXTENSION START 1584601 + if ($_[0] =~ /child connection forced to terminate due to client_idle_limit/) { + Bugzilla->check_dbh(); + ThrowUserError("request_timeout"); + } + ## RED HAT EXTENSION END 1584601 + ## RED HAT EXTENSION START 1302094 + if ($_[0] =~ m/ERROR:\s*invalid/) { + $_[0] =~ m{(ERROR:[^\[]*)}s; + my $txt = $1; + Bugzilla->logger->error($_[0]); + ThrowUserError("db_invalid_input", {err => $txt}); + } + ## RED HAT EXTENSION END 1302094 + } + + # Cut down the error string to a reasonable size + $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) + if length($_[0]) > 4000; + $_[0] = $_[0] . "\n<pre>\n" . Carp::longmess() . "\n</pre>"; + + if (!Bugzilla->request_cache->{in_error} + && Bugzilla->usage_mode == USAGE_MODE_BROWSER) + { + Bugzilla->request_cache->{in_error} = 1; +## REDHAT EXTENSION START 1174529 + my $error = 'db_error'; + $error = 'db_query_timout' + if ($_[0] =~ /canceling statement due to statement timeout/); +## REDHAT EXTENSION END 1174529 + Bugzilla->check_dbh(); ## RED HAT EXTENSION 1584601 + ThrowCodeError($error, {err_message => $_[0]}); + } + + return 0; # Now let DBI handle raising the error } sub bz_check_requirements { - my ($output) = @_; + my ($output) = @_; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; - # Only certain values are allowed for $db_driver. - if (!defined $db) { - die "$lc->{db_driver} is not a valid choice for \$db_driver in" - . bz_locations()->{'localconfig'}; - } + # Only certain values are allowed for $db_driver. + if (!defined $db) { + die "$lc->{db_driver} is not a valid choice for \$db_driver in" + . bz_locations()->{'localconfig'}; + } - # Check the existence and version of the DBD that we need. - my $dbd = $db->{dbd}; - _bz_check_dbd($db, $output); + # Check the existence and version of the DBD that we need. + my $dbd = $db->{dbd}; + _bz_check_dbd($db, $output); - # We don't try to connect to the actual database if $db_check is - # disabled. - unless ($lc->{db_check}) { - print "\n" if $output; - return; - } + # We don't try to connect to the actual database if $db_check is + # disabled. + unless ($lc->{db_check}) { + print "\n" if $output; + return; + } - # And now check the version of the database server itself. - my $dbh = _get_no_db_connection(); - $dbh->bz_check_server_version($db, $output); + # And now check the version of the database server itself. + my $dbh = _get_no_db_connection(); + $dbh->bz_check_server_version($db, $output); - print "\n" if $output; + print "\n" if $output; } sub _bz_check_dbd { - my ($db, $output) = @_; + my ($db, $output) = @_; - my $dbd = $db->{dbd}; - unless (have_vers($dbd, $output)) { - my $sql_server = $db->{name}; - my $command = install_command($dbd); - my $root = ROOT_USER; - my $dbd_mod = $dbd->{module}; - my $dbd_ver = $dbd->{version}; - die <<EOT; + my $dbd = $db->{dbd}; + unless (have_vers($dbd, $output)) { + my $sql_server = $db->{name}; + my $command = install_command($dbd); + my $root = ROOT_USER; + my $dbd_mod = $dbd->{module}; + my $dbd_ver = $dbd->{version}; + die <<EOT; For $sql_server, Bugzilla requires that perl's $dbd_mod $dbd_ver or later be installed. To install this module, run the following command (as $root): @@ -194,103 +276,107 @@ installed. To install this module, run the following command (as $root): $command EOT - } + } } sub bz_check_server_version { - my ($self, $db, $output) = @_; + my ($self, $db, $output) = @_; - my $sql_vers = $self->bz_server_version; - $self->disconnect; + my $sql_vers = $self->bz_server_version; + $self->disconnect; - my $sql_want = $db->{db_version}; - my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; + my $sql_want = $db->{db_version}; + my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; - my $sql_server = $db->{name}; - if ($output) { - Bugzilla::Install::Requirements::_checking_for({ - package => $sql_server, wanted => $sql_want, - found => $sql_vers, ok => $version_ok }); - } + my $sql_server = $db->{name}; + if ($output) { + Bugzilla::Install::Requirements::_checking_for({ + package => $sql_server, + wanted => $sql_want, + found => $sql_vers, + ok => $version_ok + }); + } - # Check what version of the database server is installed and let - # the user know if the version is too old to be used with Bugzilla. - if (!$version_ok) { - die <<EOT; + # Check what version of the database server is installed and let + # the user know if the version is too old to be used with Bugzilla. + if (!$version_ok) { + die <<EOT; Your $sql_server v$sql_vers is too old. Bugzilla requires version $sql_want or later of $sql_server. Please download and install a newer version. EOT - } + } - # This is used by subclasses. - return $sql_vers; + # This is used by subclasses. + return $sql_vers; } # Note that this function requires that localconfig exist and # be valid. sub bz_create_database { - my $dbh; - # See if we can connect to the actual Bugzilla database. - my $conn_success = eval { $dbh = connect_main() }; - my $db_name = Bugzilla->localconfig->{db_name}; - - if (!$conn_success) { - $dbh = _get_no_db_connection(); - say "Creating database $db_name..."; - - # Try to create the DB, and if we fail print a friendly error. - my $success = eval { - my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); - # This ends with 1 because this particular do doesn't always - # return something. - $dbh->do($_) foreach @sql; 1; - }; - if (!$success) { - my $error = $dbh->errstr || $@; - chomp($error); - die "The '$db_name' database could not be created.", - " The error returned was:\n\n $error\n\n", - _bz_connect_error_reasons(); - } + my $dbh; + + # See if we can connect to the actual Bugzilla database. + my $conn_success = eval { $dbh = connect_main() }; + my $db_name = Bugzilla->localconfig->{db_name}; + + if (!$conn_success) { + $dbh = _get_no_db_connection(); + say "Creating database $db_name..."; + + # Try to create the DB, and if we fail print a friendly error. + my $success = eval { + my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); + + # This ends with 1 because this particular do doesn't always + # return something. + $dbh->do($_) foreach @sql; + 1; + }; + if (!$success) { + my $error = $dbh->errstr || $@; + chomp($error); + die "The '$db_name' database could not be created.", + " The error returned was:\n\n $error\n\n", _bz_connect_error_reasons(); } + } - $dbh->disconnect; + $dbh->disconnect; } # A helper for bz_create_database and bz_check_requirements. sub _get_no_db_connection { - my ($sql_server) = @_; - my $dbh; - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_name} = ''; - my $conn_success = eval { - $dbh = _connect(\%connect_params); - }; - if (!$conn_success) { - my $driver = $connect_params{db_driver}; - my $sql_server = DB_MODULE->{lc($driver)}->{name}; - # Can't use $dbh->errstr because $dbh is undef. - my $error = $DBI::errstr || $@; - chomp($error); - die "There was an error connecting to $sql_server:\n\n", - " $error\n\n", _bz_connect_error_reasons(), "\n"; - } - return $dbh; + my ($sql_server) = @_; + my $dbh; + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_name} = ''; + my $conn_success = eval { $dbh = _connect(\%connect_params); }; + if (!$conn_success) { + my $driver = $connect_params{db_driver}; + my $sql_server = DB_MODULE->{lc($driver)}->{name}; + + # Can't use $dbh->errstr because $dbh is undef. + my $error = $DBI::errstr || $@; + chomp($error); + die "There was an error connecting to $sql_server:\n\n", " $error\n\n", + _bz_connect_error_reasons(), "\n"; + } + return $dbh; } # Just a helper because we have to re-use this text. # We don't use this in db_new because it gives away the database # username, and db_new errors can show up on CGIs. sub _bz_connect_error_reasons { - my $lc_file = bz_locations()->{'localconfig'}; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; - my $server = $db->{name}; + my $lc_file = bz_locations()->{'localconfig'}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $server = $db->{name}; -return <<EOT; + return <<EOT; This might have several reasons: * $server is not running. @@ -309,154 +395,157 @@ EOT # List of abstract methods we are checking the derived class implements our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit sql_to_days - sql_date_format sql_date_math bz_explain - sql_group_concat); + sql_date_format sql_date_math bz_explain + sql_group_concat); # This overridden import method will check implementation of inherited classes # for missing implementation of abstract methods # See http://perlmonks.thepen.com/44265.html sub import { - my $pkg = shift; - - # do not check this module - if ($pkg ne __PACKAGE__) { - # make sure all abstract methods are implemented - foreach my $meth (@_abstract_methods) { - $pkg->can($meth) - or die("Class $pkg does not define method $meth"); - } + my $pkg = shift; + + # do not check this module + if ($pkg ne __PACKAGE__) { + + # make sure all abstract methods are implemented + foreach my $meth (@_abstract_methods) { + $pkg->can($meth) or die("Class $pkg does not define method $meth"); } + } - # Now we want to call our superclass implementation. - # If our superclass is Exporter, which is using caller() to find - # a namespace to populate, we need to adjust for this extra call. - # All this can go when we stop using deprecated functions. - my $is_exporter = $pkg->isa('Exporter'); - $Exporter::ExportLevel++ if $is_exporter; - $pkg->SUPER::import(@_); - $Exporter::ExportLevel-- if $is_exporter; + # Now we want to call our superclass implementation. + # If our superclass is Exporter, which is using caller() to find + # a namespace to populate, we need to adjust for this extra call. + # All this can go when we stop using deprecated functions. + my $is_exporter = $pkg->isa('Exporter'); + $Exporter::ExportLevel++ if $is_exporter; + $pkg->SUPER::import(@_); + $Exporter::ExportLevel-- if $is_exporter; } sub sql_istrcmp { - my ($self, $left, $right, $op) = @_; - $op ||= "="; + my ($self, $left, $right, $op) = @_; + $op ||= "="; - return $self->sql_istring($left) . " $op " . $self->sql_istring($right); + return $self->sql_istring($left) . " $op " . $self->sql_istring($right); } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER($string)"; + return "LOWER($string)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - $fragment = $self->sql_istring($fragment); - $text = $self->sql_istring($text); - return $self->sql_position($fragment, $text); + my ($self, $fragment, $text) = @_; + $fragment = $self->sql_istring($fragment); + $text = $self->sql_istring($text); + return $self->sql_position($fragment, $text); } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION($fragment IN $text)"; + return "POSITION($fragment IN $text)"; } sub sql_like { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_position($quoted, $column) . " > 0"; + return $self->sql_position($quoted, $column) . " > 0"; } sub sql_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " > 0"; + return $self->sql_iposition($quoted, $column) . " > 0"; } sub sql_not_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " = 0"; + return $self->sql_iposition($quoted, $column) . " = 0"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; + + my $expression = "GROUP BY $needed_columns"; + $expression .= ", " . $optional_columns if $optional_columns; - my $expression = "GROUP BY $needed_columns"; - $expression .= ", " . $optional_columns if $optional_columns; - - return $expression; + return $expression; } sub sql_string_concat { - my ($self, @params) = @_; - - return '(' . join(' || ', @params) . ')'; + my ($self, @params) = @_; + + return '(' . join(' || ', @params) . ')'; } sub sql_string_until { - my ($self, $string, $substring) = @_; + my ($self, $string, $substring) = @_; - my $position = $self->sql_position($substring, $string); - return "CASE WHEN $position != 0" - . " THEN SUBSTR($string, 1, $position - 1)" - . " ELSE $string END"; + my $position = $self->sql_position($substring, $string); + return + "CASE WHEN $position != 0" + . " THEN SUBSTR($string, 1, $position - 1)" + . " ELSE $string END"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - return " $column_name " - . ($negate ? "NOT " : "") - . "IN (" . join(',', @$in_list_ref) . ") "; + my ($self, $column_name, $in_list_ref, $negate) = @_; + return + " $column_name " + . ($negate ? "NOT " : "") . "IN (" + . join(',', @$in_list_ref) . ") "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # This is as close as we can get to doing full text search using - # standard ANSI SQL, without real full text search support. DB specific - # modules should override this, as this will be always much slower. - - # make the string lowercase to do case insensitive search - my $lower_text = lc($text); - - # split the text we're searching for into separate words. As a hack - # to allow quicksearch to work, if the field starts and ends with - # a double-quote, then we don't split it into words. We can't use - # Text::ParseWords here because it gets very confused by unbalanced - # quotes, which breaks searches like "don't try this" (because of the - # unbalanced single-quote in "don't"). - my @words; - if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { - $lower_text =~ s/^"//; - $lower_text =~ s/"$//; - @words = ($lower_text); - } - else { - @words = split(/\s+/, $lower_text); - } - - # surround the words with wildcards and SQL quotes so we can use them - # in LIKE search clauses - @words = map($self->quote("\%$_\%"), @words); - - # untaint words, since they are safe to use now that we've quoted them - trick_taint($_) foreach @words; - - # turn the words into a set of LIKE search clauses - @words = map("LOWER($column) LIKE $_", @words); - - # search for occurrences of all specified words in the column - return join (" AND ", @words), "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; + my ($self, $column, $text) = @_; + + # This is as close as we can get to doing full text search using + # standard ANSI SQL, without real full text search support. DB specific + # modules should override this, as this will be always much slower. + + # make the string lowercase to do case insensitive search + my $lower_text = lc($text); + + # split the text we're searching for into separate words. As a hack + # to allow quicksearch to work, if the field starts and ends with + # a double-quote, then we don't split it into words. We can't use + # Text::ParseWords here because it gets very confused by unbalanced + # quotes, which breaks searches like "don't try this" (because of the + # unbalanced single-quote in "don't"). + my @words; + if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { + $lower_text =~ s/^"//; + $lower_text =~ s/"$//; + @words = ($lower_text); + } + else { + @words = split(/\s+/, $lower_text); + } + + # surround the words with wildcards and SQL quotes so we can use them + # in LIKE search clauses + @words = map($self->quote("\%$_\%"), @words); + + # untaint words, since they are safe to use now that we've quoted them + trick_taint($_) foreach @words; + + # turn the words into a set of LIKE search clauses + @words = map("LOWER($column) LIKE $_", @words); + + # search for occurrences of all specified words in the column + return join(" AND ", @words), + "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; } ##################################################################### @@ -465,24 +554,27 @@ sub sql_fulltext_search { # XXX - Needs to be documented. sub bz_server_version { - my ($self) = @_; - return $self->get_info(18); # SQL_DBMS_VER + my ($self) = @_; + return $self->get_info(18); # SQL_DBMS_VER } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - return $self->last_insert_id(Bugzilla->localconfig->{db_name}, undef, - $table, $column); + return $self->last_insert_id(Bugzilla->localconfig->{db_name}, + undef, $table, $column); } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } ##################################################################### @@ -490,99 +582,100 @@ sub bz_check_regexp { ##################################################################### sub bz_setup_database { - my ($self) = @_; - - # If we haven't ever stored a serialized schema, - # set up the bz_schema table and store it. - $self->_bz_init_schema_storage(); - - # We don't use bz_table_list here, because that uses _bz_real_schema. - # We actually want the table list from the ABSTRACT_SCHEMA in - # Bugzilla::DB::Schema. - my @desired_tables = $self->_bz_schema->get_table_list(); - my $bugs_exists = $self->bz_table_info('bugs'); - if (!$bugs_exists) { - say install_string('db_table_setup'); - } + my ($self) = @_; - foreach my $table_name (@desired_tables) { - $self->bz_add_table($table_name, { silently => !$bugs_exists }); - } + # If we haven't ever stored a serialized schema, + # set up the bz_schema table and store it. + $self->_bz_init_schema_storage(); + + # We don't use bz_table_list here, because that uses _bz_real_schema. + # We actually want the table list from the ABSTRACT_SCHEMA in + # Bugzilla::DB::Schema. + my @desired_tables = $self->_bz_schema->get_table_list(); + my $bugs_exists = $self->bz_table_info('bugs'); + if (!$bugs_exists) { + say install_string('db_table_setup'); + } + + foreach my $table_name (@desired_tables) { + $self->bz_add_table($table_name, {silently => !$bugs_exists}); + } } # This really just exists to get overridden in Bugzilla::DB::Mysql. sub bz_enum_initial_values { - return ENUM_DEFAULTS; + return ENUM_DEFAULTS; } sub bz_populate_enum_tables { - my ($self) = @_; + my ($self) = @_; - my $any_severities = $self->selectrow_array( - 'SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); - print install_string('db_enum_setup'), "\n " if !$any_severities; + my $any_severities + = $self->selectrow_array('SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); + print install_string('db_enum_setup'), "\n " if !$any_severities; - my $enum_values = $self->bz_enum_initial_values(); - while (my ($table, $values) = each %$enum_values) { - $self->_bz_populate_enum_table($table, $values); - } + my $enum_values = $self->bz_enum_initial_values(); + while (my ($table, $values) = each %$enum_values) { + $self->_bz_populate_enum_table($table, $values); + } - print "\n" if !$any_severities; + print "\n" if !$any_severities; } sub bz_setup_foreign_keys { - my ($self) = @_; - - # profiles_activity was the first table to get foreign keys, - # so if it doesn't have them, then we're setting up FKs - # for the first time, and should be quieter about it. - my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); - my $any_fks = $activity_fk && $activity_fk->{created}; - if (!$any_fks) { - say get_text('install_fk_setup'); - } + my ($self) = @_; + + # profiles_activity was the first table to get foreign keys, + # so if it doesn't have them, then we're setting up FKs + # for the first time, and should be quieter about it. + my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); + my $any_fks = $activity_fk && $activity_fk->{created}; + if (!$any_fks) { + say get_text('install_fk_setup'); + } + + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + my %add_fks; + foreach my $column (@columns) { - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - my %add_fks; - foreach my $column (@columns) { - # First we check for any FKs that have created => 0, - # in the _bz_real_schema. This also picks up FKs with - # created => 1, but bz_add_fks will ignore those. - my $fk = $self->bz_fk_info($table, $column); - # Then we check the abstract schema to see if there - # should be an FK on this column, but one wasn't set in the - # _bz_real_schema for some reason. We do this to handle - # various problems caused by upgrading from versions - # prior to 4.2, and also to handle problems caused - # by enabling an extension pre-4.2, disabling it for - # the 4.2 upgrade, and then re-enabling it later. - unless ($fk && $fk->{created}) { - my $standard_def = - $self->_bz_schema->get_column_abstract($table, $column); - if (exists $standard_def->{REFERENCES}) { - $fk = dclone($standard_def->{REFERENCES}); - } - } - - $add_fks{$column} = $fk if $fk; + # First we check for any FKs that have created => 0, + # in the _bz_real_schema. This also picks up FKs with + # created => 1, but bz_add_fks will ignore those. + my $fk = $self->bz_fk_info($table, $column); + + # Then we check the abstract schema to see if there + # should be an FK on this column, but one wasn't set in the + # _bz_real_schema for some reason. We do this to handle + # various problems caused by upgrading from versions + # prior to 4.2, and also to handle problems caused + # by enabling an extension pre-4.2, disabling it for + # the 4.2 upgrade, and then re-enabling it later. + unless ($fk && $fk->{created}) { + my $standard_def = $self->_bz_schema->get_column_abstract($table, $column); + if (exists $standard_def->{REFERENCES}) { + $fk = dclone($standard_def->{REFERENCES}); } - $self->bz_add_fks($table, \%add_fks, { silently => !$any_fks }); + } + + $add_fks{$column} = $fk if $fk; } + $self->bz_add_fks($table, \%add_fks, {silently => !$any_fks}); + } } # This is used by contrib/bzdbcopy.pl, mostly. sub bz_drop_foreign_keys { - my ($self) = @_; + my ($self) = @_; - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); } + } } ##################################################################### @@ -590,119 +683,121 @@ sub bz_drop_foreign_keys { ##################################################################### sub bz_add_column { - my ($self, $table, $name, $new_def, $init_value) = @_; - - # You can't add a NOT NULL column to a table with - # no DEFAULT statement, unless you have an init_value. - # SERIAL types are an exception, though, because they can - # auto-populate. - if ( $new_def->{NOTNULL} && !exists $new_def->{DEFAULT} - && !defined $init_value && $new_def->{TYPE} !~ /SERIAL/) - { - ThrowCodeError('column_not_null_without_default', - { name => "$table.$name" }); + my ($self, $table, $name, $new_def, $init_value) = @_; + + # You can't add a NOT NULL column to a table with + # no DEFAULT statement, unless you have an init_value. + # SERIAL types are an exception, though, because they can + # auto-populate. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $init_value + && $new_def->{TYPE} !~ /SERIAL/) + { + ThrowCodeError('column_not_null_without_default', {name => "$table.$name"}); + } + + my $current_def = $self->bz_column_info($table, $name); + + if (!$current_def) { + + # REFERENCES need to happen later and not be created right away + my $trimmed_def = dclone($new_def); + delete $trimmed_def->{REFERENCES}; + my @statements + = $self->_bz_real_schema->get_add_column_ddl($table, $name, $trimmed_def, + defined $init_value ? $self->quote($init_value) : undef); + print get_text('install_column_add', {column => $name, table => $table}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + $self->do($sql); } - my $current_def = $self->bz_column_info($table, $name); - - if (!$current_def) { - # REFERENCES need to happen later and not be created right away - my $trimmed_def = dclone($new_def); - delete $trimmed_def->{REFERENCES}; - my @statements = $self->_bz_real_schema->get_add_column_ddl( - $table, $name, $trimmed_def, - defined $init_value ? $self->quote($init_value) : undef); - print get_text('install_column_add', - { column => $name, table => $table }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - - # To make things easier for callers, if they don't specify - # a REFERENCES item, we pull it from the _bz_schema if the - # column exists there and has a REFERENCES item. - # bz_setup_foreign_keys will then add this FK at the end of - # Install::DB. - my $col_abstract = - $self->_bz_schema->get_column_abstract($table, $name); - if (exists $col_abstract->{REFERENCES}) { - my $new_fk = dclone($col_abstract->{REFERENCES}); - $new_fk->{created} = 0; - $new_def->{REFERENCES} = $new_fk; - } - - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + # To make things easier for callers, if they don't specify + # a REFERENCES item, we pull it from the _bz_schema if the + # column exists there and has a REFERENCES item. + # bz_setup_foreign_keys will then add this FK at the end of + # Install::DB. + my $col_abstract = $self->_bz_schema->get_column_abstract($table, $name); + if (exists $col_abstract->{REFERENCES}) { + my $new_fk = dclone($col_abstract->{REFERENCES}); + $new_fk->{created} = 0; + $new_def->{REFERENCES} = $new_fk; } + + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } sub bz_add_fk { - my ($self, $table, $column, $def) = @_; - $self->bz_add_fks($table, { $column => $def }); + my ($self, $table, $column, $def) = @_; + $self->bz_add_fks($table, {$column => $def}); } sub bz_add_fks { - my ($self, $table, $column_fks, $options) = @_; - - my %add_these; - foreach my $column (keys %$column_fks) { - my $current_fk = $self->bz_fk_info($table, $column); - next if ($current_fk and $current_fk->{created}); - my $new_fk = $column_fks->{$column}; - $self->_check_references($table, $column, $new_fk); - $add_these{$column} = $new_fk; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - print get_text('install_fk_add', - { table => $table, column => $column, - fk => $new_fk }), "\n"; - } + my ($self, $table, $column_fks, $options) = @_; + + my %add_these; + foreach my $column (keys %$column_fks) { + my $current_fk = $self->bz_fk_info($table, $column); + next if ($current_fk and $current_fk->{created}); + my $new_fk = $column_fks->{$column}; + $self->_check_references($table, $column, $new_fk); + $add_these{$column} = $new_fk; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + print get_text( + 'install_fk_add', {table => $table, column => $column, fk => $new_fk} + ), + "\n"; } + } - return if !scalar(keys %add_these); + return if !scalar(keys %add_these); - my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); - $self->do($_) foreach @sql; + my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); + $self->do($_) foreach @sql; - foreach my $column (keys %add_these) { - my $fk_def = $add_these{$column}; - $fk_def->{created} = 1; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - } + foreach my $column (keys %add_these) { + my $fk_def = $add_these{$column}; + $fk_def->{created} = 1; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + } - $self->_bz_store_real_schema(); + $self->_bz_store_real_schema(); } sub bz_alter_column { - my ($self, $table, $name, $new_def, $set_nulls_to) = @_; + my ($self, $table, $name, $new_def, $set_nulls_to) = @_; - my $current_def = $self->bz_column_info($table, $name); + my $current_def = $self->bz_column_info($table, $name); - if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { - # You can't change a column to be NOT NULL if you have no DEFAULT - # and no value for $set_nulls_to, if there are any NULL values - # in that column. - if ($new_def->{NOTNULL} && - !exists $new_def->{DEFAULT} && !defined $set_nulls_to) - { - # Check for NULLs - my $any_nulls = $self->selectrow_array( - "SELECT 1 FROM $table WHERE $name IS NULL"); - ThrowCodeError('column_not_null_no_default_alter', - { name => "$table.$name" }) if ($any_nulls); - } - # Preserve foreign key definitions in the Schema object when altering - # types. - if (my $fk = $self->bz_fk_info($table, $name)) { - $new_def->{REFERENCES} = $fk; - } - $self->bz_alter_column_raw($table, $name, $new_def, $current_def, - $set_nulls_to); - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { + + # You can't change a column to be NOT NULL if you have no DEFAULT + # and no value for $set_nulls_to, if there are any NULL values + # in that column. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $set_nulls_to) + { + # Check for NULLs + my $any_nulls + = $self->selectrow_array("SELECT 1 FROM $table WHERE $name IS NULL"); + ThrowCodeError('column_not_null_no_default_alter', {name => "$table.$name"}) + if ($any_nulls); + } + + # Preserve foreign key definitions in the Schema object when altering + # types. + if (my $fk = $self->bz_fk_info($table, $name)) { + $new_def->{REFERENCES} = $fk; } + $self->bz_alter_column_raw($table, $name, $new_def, $current_def, + $set_nulls_to); + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } @@ -728,39 +823,40 @@ sub bz_alter_column { # Returns: nothing # sub bz_alter_column_raw { - my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; - my @statements = $self->_bz_real_schema->get_alter_column_ddl( - $table, $name, $new_def, - defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); - my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); - say "Updating column $name in table $table ..."; - if (defined $current_def) { - my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); - say "Old: $old_ddl"; - } - say "New: $new_ddl"; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; + my @statements + = $self->_bz_real_schema->get_alter_column_ddl($table, $name, $new_def, + defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); + my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); + say "Updating column $name in table $table ..."; + if (defined $current_def) { + my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); + say "Old: $old_ddl"; + } + say "New: $new_ddl"; + $self->do($_) foreach (@statements); } sub bz_alter_fk { - my ($self, $table, $column, $fk_def) = @_; - my $current_fk = $self->bz_fk_info($table, $column); - ThrowCodeError('column_alter_nonexistent_fk', - { table => $table, column => $column }) if !$current_fk; - $self->bz_drop_fk($table, $column); - $self->bz_add_fk($table, $column, $fk_def); + my ($self, $table, $column, $fk_def) = @_; + my $current_fk = $self->bz_fk_info($table, $column); + ThrowCodeError('column_alter_nonexistent_fk', + {table => $table, column => $column}) + if !$current_fk; + $self->bz_drop_fk($table, $column); + $self->bz_add_fk($table, $column, $fk_def); } sub bz_add_index { - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if (!$index_exists) { - $self->bz_add_index_raw($table, $name, $definition); - $self->_bz_real_schema->set_index($table, $name, $definition); - $self->_bz_store_real_schema; - } + if (!$index_exists) { + $self->bz_add_index_raw($table, $name, $definition); + $self->_bz_real_schema->set_index($table, $name, $definition); + $self->_bz_store_real_schema; + } } # bz_add_index_raw($table, $name, $silent) @@ -780,36 +876,36 @@ sub bz_add_index { # Returns: nothing # sub bz_add_index_raw { - my ($self, $table, $name, $definition, $silent) = @_; - my @statements = $self->_bz_schema->get_add_index_ddl( - $table, $name, $definition); - print "Adding new index '$name' to the $table table ...\n" unless $silent; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $definition, $silent) = @_; + my @statements + = $self->_bz_schema->get_add_index_ddl($table, $name, $definition); + print "Adding new index '$name' to the $table table ...\n" unless $silent; + $self->do($_) foreach (@statements); } sub bz_add_table { - my ($self, $name, $options) = @_; - - my $table_exists = $self->bz_table_info($name); - - if (!$table_exists) { - $self->_bz_add_table_raw($name, $options); - my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); - - my %fields = @{$table_def->{FIELDS}}; - foreach my $col (keys %fields) { - # Foreign Key references have to be added by Install::DB after - # initial table creation, because column names have changed - # over history and it's impossible to keep track of that info - # in ABSTRACT_SCHEMA. - next unless exists $fields{$col}->{REFERENCES}; - $fields{$col}->{REFERENCES}->{created} = - $self->_bz_real_schema->FK_ON_CREATE; - } - - $self->_bz_real_schema->add_table($name, $table_def); - $self->_bz_store_real_schema; + my ($self, $name, $options) = @_; + + my $table_exists = $self->bz_table_info($name); + + if (!$table_exists) { + $self->_bz_add_table_raw($name, $options); + my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); + + my %fields = @{$table_def->{FIELDS}}; + foreach my $col (keys %fields) { + + # Foreign Key references have to be added by Install::DB after + # initial table creation, because column names have changed + # over history and it's impossible to keep track of that info + # in ABSTRACT_SCHEMA. + next unless exists $fields{$col}->{REFERENCES}; + $fields{$col}->{REFERENCES}->{created} = $self->_bz_real_schema->FK_ON_CREATE; } + + $self->_bz_real_schema->add_table($name, $table_def); + $self->_bz_store_real_schema; + } } # _bz_add_table_raw($name) - Private @@ -821,164 +917,190 @@ sub bz_add_table { # _bz_init_schema_storage. Used when you don't # yet have a Schema object but you need to # add a table, for some reason. -# Params: $name - The name of the table you're creating. -# The definition for the table is pulled from +# Params: $name - The name of the table you're creating. +# The definition for the table is pulled from # _bz_schema. # Returns: nothing # sub _bz_add_table_raw { - my ($self, $name, $options) = @_; - my @statements = $self->_bz_schema->get_table_ddl($name); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - say install_string('db_table_new', { table => $name }); - } - $self->do($_) foreach (@statements); + my ($self, $name, $options) = @_; + my @statements = $self->_bz_schema->get_table_ddl($name); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + say install_string('db_table_new', {table => $name}); + } + $self->do($_) foreach (@statements); } sub _bz_add_field_table { - my ($self, $name, $schema_ref) = @_; - # We do nothing if the table already exists. - return if $self->bz_table_info($name); - - # Copy this so that we're not modifying the passed reference. - # (This avoids modifying a constant in Bugzilla::DB::Schema.) - my %table_schema = %$schema_ref; - my %indexes = @{ $table_schema{INDEXES} }; - my %fixed_indexes; - foreach my $key (keys %indexes) { - $fixed_indexes{$name . "_" . $key} = $indexes{$key}; - } - # INDEXES is supposed to be an arrayref, so we have to convert back. - my @indexes_array = %fixed_indexes; - $table_schema{INDEXES} = \@indexes_array; - # We add this to the abstract schema so that bz_add_table can find it. - $self->_bz_schema->add_table($name, \%table_schema); - $self->bz_add_table($name); + my ($self, $name, $schema_ref) = @_; + + # We do nothing if the table already exists. + return if $self->bz_table_info($name); + + # Copy this so that we're not modifying the passed reference. + # (This avoids modifying a constant in Bugzilla::DB::Schema.) + my %table_schema = %$schema_ref; + my %indexes = @{$table_schema{INDEXES}}; + my %fixed_indexes; + foreach my $key (keys %indexes) { + $fixed_indexes{$name . "_" . $key} = $indexes{$key}; + } + + # INDEXES is supposed to be an arrayref, so we have to convert back. + my @indexes_array = %fixed_indexes; + $table_schema{INDEXES} = \@indexes_array; + + # We add this to the abstract schema so that bz_add_table can find it. + $self->_bz_schema->add_table($name, \%table_schema); + $self->bz_add_table($name); } sub bz_add_field_tables { - my ($self, $field) = @_; - - $self->_bz_add_field_table($field->name, - $self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type); - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - my $ms_table = "bug_" . $field->name; - $self->_bz_add_field_table($ms_table, - $self->_bz_schema->MULTI_SELECT_VALUE_TABLE); - - $self->bz_add_fks($ms_table, - { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', - DELETE => 'CASCADE'}, - - value => {TABLE => $field->name, COLUMN => 'value'} }); - } + my ($self, $field) = @_; + + $self->_bz_add_field_table($field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA, + $field->type); + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + my $ms_table = "bug_" . $field->name; + $self->_bz_add_field_table($ms_table, + $self->_bz_schema->MULTI_SELECT_VALUE_TABLE); + + $self->bz_add_fks( + $ms_table, + { + bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + + value => {TABLE => $field->name, COLUMN => 'value'} + } + ); + } + + if ($field->type == FIELD_TYPE_ONE_SELECT) { + my $ms_table = "bug_" . $field->name; + $self->_bz_add_field_table($ms_table, + $self->_bz_schema->SELECT_ONE_VALUE_TABLE); + + $self->bz_add_fks( + $ms_table, + { + bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + value => {TABLE => $field->name, COLUMN => 'value'} + } + ); + } } sub bz_drop_field_tables { - my ($self, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - $self->bz_drop_table('bug_' . $field->name); - } - $self->bz_drop_table($field->name); + my ($self, $field) = @_; + if ( $field->type == FIELD_TYPE_MULTI_SELECT + || $field->type == FIELD_TYPE_ONE_SELECT) + { + $self->bz_drop_table('bug_' . $field->name); + } + $self->bz_drop_table($field->name); } sub bz_drop_column { - my ($self, $table, $column) = @_; - - my $current_def = $self->bz_column_info($table, $column); - - if ($current_def) { - my @statements = $self->_bz_real_schema->get_drop_column_ddl( - $table, $column); - print get_text('install_column_drop', - { table => $table, column => $column }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_column($table, $column); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $current_def = $self->bz_column_info($table, $column); + + if ($current_def) { + my @statements = $self->_bz_real_schema->get_drop_column_ddl($table, $column); + print get_text('install_column_drop', {table => $table, column => $column}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_column($table, $column); + $self->_bz_store_real_schema; + } } sub bz_drop_fk { - my ($self, $table, $column) = @_; - - my $fk_def = $self->bz_fk_info($table, $column); - if ($fk_def and $fk_def->{created}) { - print get_text('install_fk_drop', - { table => $table, column => $column, fk => $fk_def }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - my @statements = - $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - # Under normal circumstances, we don't permanently drop the fk-- - # we want checksetup to re-create it again later. The only - # time that FKs get permanently dropped is if the column gets - # dropped. - $fk_def->{created} = 0; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $fk_def = $self->bz_fk_info($table, $column); + if ($fk_def and $fk_def->{created}) { + print get_text('install_fk_drop', + {table => $table, column => $column, fk => $fk_def}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + my @statements + = $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + # Under normal circumstances, we don't permanently drop the fk-- + # we want checksetup to re-create it again later. The only + # time that FKs get permanently dropped is if the column gets + # dropped. + $fk_def->{created} = 0; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + $self->_bz_store_real_schema; + } + } sub bz_get_related_fks { - my ($self, $table, $column) = @_; - my @tables = $self->_bz_real_schema->get_table_list(); - my @related; - foreach my $check_table (@tables) { - my @columns = $self->bz_table_columns($check_table); - foreach my $check_column (@columns) { - my $fk = $self->bz_fk_info($check_table, $check_column); - if ($fk - and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) - or ($check_column eq $column and $check_table eq $table))) - { - push(@related, [$check_table, $check_column, $fk]); - } - } # foreach $column - } # foreach $table - - return \@related; + my ($self, $table, $column) = @_; + my @tables = $self->_bz_real_schema->get_table_list(); + my @related; + foreach my $check_table (@tables) { + my @columns = $self->bz_table_columns($check_table); + foreach my $check_column (@columns) { + my $fk = $self->bz_fk_info($check_table, $check_column); + if ( + $fk + and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) + or ($check_column eq $column and $check_table eq $table)) + ) + { + push(@related, [$check_table, $check_column, $fk]); + } + } # foreach $column + } # foreach $table + + return \@related; } sub bz_drop_related_fks { - my $self = shift; - my $related = $self->bz_get_related_fks(@_); - foreach my $item (@$related) { - my ($table, $column) = @$item; - $self->bz_drop_fk($table, $column); - } - return $related; + my $self = shift; + my $related = $self->bz_get_related_fks(@_); + foreach my $item (@$related) { + my ($table, $column) = @$item; + $self->bz_drop_fk($table, $column); + } + return $related; } sub bz_drop_index { - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if ($index_exists) { - if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { - # We cannot delete an index used by a FK. - foreach my $column (@{$index_exists->{FIELDS}}) { - $self->bz_drop_related_fks($table, $column); - } - } - $self->bz_drop_index_raw($table, $name); - $self->_bz_real_schema->delete_index($table, $name); - $self->_bz_store_real_schema; + if ($index_exists) { + if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { + + # We cannot delete an index used by a FK. + foreach my $column (@{$index_exists->{FIELDS}}) { + $self->bz_drop_related_fks($table, $column); + } } + $self->bz_drop_index_raw($table, $name); + $self->_bz_real_schema->delete_index($table, $name); + $self->_bz_store_real_schema; + } } # bz_drop_index_raw($table, $name, $silent) @@ -987,7 +1109,7 @@ sub bz_drop_index { # Drops an index from the database # without updating any Schema object. Generally # should only be called by bz_drop_index. -# Used when either: (1) You don't yet have a Schema +# Used when either: (1) You don't yet have a Schema # object but you need to drop an index, for some reason. # (2) You need to drop an index that somehow got into the # database but doesn't exist in Schema. @@ -998,108 +1120,111 @@ sub bz_drop_index { # Returns: nothing # sub bz_drop_index_raw { - my ($self, $table, $name, $silent) = @_; - my @statements = $self->_bz_schema->get_drop_index_ddl( - $table, $name); - print "Removing index '$name' from the $table table...\n" unless $silent; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; - } + my ($self, $table, $name, $silent) = @_; + my @statements = $self->_bz_schema->get_drop_index_ddl($table, $name); + print "Removing index '$name' from the $table table...\n" unless $silent; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; + } } sub bz_drop_table { - my ($self, $name) = @_; - - my $table_exists = $self->bz_table_info($name); - - if ($table_exists) { - my @statements = $self->_bz_schema->get_drop_table_ddl($name); - print get_text('install_table_drop', { name => $name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_table($name); - $self->_bz_store_real_schema; + my ($self, $name) = @_; + + my $table_exists = $self->bz_table_info($name); + + if ($table_exists) { + my @statements = $self->_bz_schema->get_drop_table_ddl($name); + print get_text('install_table_drop', {name => $name}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_table($name); + $self->_bz_store_real_schema; + } } sub bz_fk_info { - my ($self, $table, $column) = @_; - my $col_info = $self->bz_column_info($table, $column); - return undef if !$col_info; - my $fk = $col_info->{REFERENCES}; - return $fk; + my ($self, $table, $column) = @_; + my $col_info = $self->bz_column_info($table, $column); + return undef if !$col_info; + my $fk = $col_info->{REFERENCES}; + return $fk; } sub bz_rename_column { - my ($self, $table, $old_name, $new_name) = @_; + my ($self, $table, $old_name, $new_name) = @_; - my $old_col_exists = $self->bz_column_info($table, $old_name); + my $old_col_exists = $self->bz_column_info($table, $old_name); - if ($old_col_exists) { - my $already_renamed = $self->bz_column_info($table, $new_name); - ThrowCodeError('db_rename_conflict', - { old => "$table.$old_name", - new => "$table.$new_name" }) if $already_renamed; - my @statements = $self->_bz_real_schema->get_rename_column_ddl( - $table, $old_name, $new_name); + if ($old_col_exists) { + my $already_renamed = $self->bz_column_info($table, $new_name); + ThrowCodeError('db_rename_conflict', + {old => "$table.$old_name", new => "$table.$new_name"}) + if $already_renamed; + my @statements + = $self->_bz_real_schema->get_rename_column_ddl($table, $old_name, $new_name); - print get_text('install_column_rename', - { old => "$table.$old_name", new => "$table.$new_name" }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + print get_text('install_column_rename', + {old => "$table.$old_name", new => "$table.$new_name"}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - $self->_bz_real_schema->rename_column($table, $old_name, $new_name); - $self->_bz_store_real_schema; + foreach my $sql (@statements) { + $self->do($sql); } + $self->_bz_real_schema->rename_column($table, $old_name, $new_name); + $self->_bz_store_real_schema; + } } sub bz_rename_table { - my ($self, $old_name, $new_name) = @_; - my $old_table = $self->bz_table_info($old_name); - return if !$old_table; - - my $new = $self->bz_table_info($new_name); - ThrowCodeError('db_rename_conflict', { old => $old_name, - new => $new_name }) if $new; - - # FKs will all have the wrong names unless we drop and then let them - # be re-created later. Under normal circumstances, checksetup.pl will - # automatically re-create these dropped FKs at the end of its DB upgrade - # run, so we don't need to re-create them in this method. - my @columns = $self->bz_table_columns($old_name); - foreach my $column (@columns) { - # these just return silently if there's no FK to drop - $self->bz_drop_fk($old_name, $column); - $self->bz_drop_related_fks($old_name, $column); - } - - my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); - print get_text('install_table_rename', - { old => $old_name, new => $new_name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - $self->do($_) foreach @sql; - $self->_bz_real_schema->rename_table($old_name, $new_name); - $self->_bz_store_real_schema; + my ($self, $old_name, $new_name) = @_; + my $old_table = $self->bz_table_info($old_name); + return if !$old_table; + + my $new = $self->bz_table_info($new_name); + ThrowCodeError('db_rename_conflict', {old => $old_name, new => $new_name}) + if $new; + + # FKs will all have the wrong names unless we drop and then let them + # be re-created later. Under normal circumstances, checksetup.pl will + # automatically re-create these dropped FKs at the end of its DB upgrade + # run, so we don't need to re-create them in this method. + my @columns = $self->bz_table_columns($old_name); + foreach my $column (@columns) { + + # these just return silently if there's no FK to drop + $self->bz_drop_fk($old_name, $column); + $self->bz_drop_related_fks($old_name, $column); + } + + my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); + print get_text('install_table_rename', {old => $old_name, new => $new_name}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + $self->do($_) foreach @sql; + $self->_bz_real_schema->rename_table($old_name, $new_name); + $self->_bz_store_real_schema; } sub bz_set_next_serial_value { - my ($self, $table, $column, $value) = @_; - if (!$value) { - $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0; - $value++; - } - my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value); - $self->do($_) foreach @sql; + my ($self, $table, $column, $value) = @_; + if (!$value) { + $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0; + $value++; + } + my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value); + $self->do($_) foreach @sql; } ##################################################################### @@ -1107,12 +1232,12 @@ sub bz_set_next_serial_value { ##################################################################### sub _bz_schema { - my ($self) = @_; - return $self->{private_bz_schema} if exists $self->{private_bz_schema}; - my @module_parts = split('::', ref $self); - my $module_name = pop @module_parts; - $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); - return $self->{private_bz_schema}; + my ($self) = @_; + return $self->{private_bz_schema} if exists $self->{private_bz_schema}; + my @module_parts = split('::', ref $self); + my $module_name = pop @module_parts; + $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); + return $self->{private_bz_schema}; } # _bz_get_initial_schema() @@ -1126,53 +1251,54 @@ sub _bz_schema { # Returns: A Schema object that can be serialized and written to disk # for _bz_init_schema_storage. sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_schema->get_empty_schema(); + my ($self) = @_; + return $self->_bz_schema->get_empty_schema(); } sub bz_column_info { - my ($self, $table, $column) = @_; - my $def = $self->_bz_real_schema->get_column_abstract($table, $column); - # We dclone it so callers can't modify the Schema. - $def = dclone($def) if defined $def; - return $def; + my ($self, $table, $column) = @_; + my $def = $self->_bz_real_schema->get_column_abstract($table, $column); + + # We dclone it so callers can't modify the Schema. + $def = dclone($def) if defined $def; + return $def; } sub bz_index_info { - my ($self, $table, $index) = @_; - my $index_def = - $self->_bz_real_schema->get_index_abstract($table, $index); - if (ref($index_def) eq 'ARRAY') { - $index_def = {FIELDS => $index_def, TYPE => ''}; - } - return $index_def; + my ($self, $table, $index) = @_; + my $index_def = $self->_bz_real_schema->get_index_abstract($table, $index); + if (ref($index_def) eq 'ARRAY') { + $index_def = {FIELDS => $index_def, TYPE => ''}; + } + return $index_def; } sub bz_table_info { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_abstract($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_abstract($table); } sub bz_table_columns { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_columns($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_columns($table); } sub bz_table_indexes { - my ($self, $table) = @_; - my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); - my %return_indexes; - # We do this so that they're always hashes. - foreach my $name (keys %$indexes) { - $return_indexes{$name} = $self->bz_index_info($table, $name); - } - return \%return_indexes; + my ($self, $table) = @_; + my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); + my %return_indexes; + + # We do this so that they're always hashes. + foreach my $name (keys %$indexes) { + $return_indexes{$name} = $self->bz_index_info($table, $name); + } + return \%return_indexes; } sub bz_table_list { - my ($self) = @_; - return $self->_bz_real_schema->get_table_list(); + my ($self) = @_; + return $self->_bz_real_schema->get_table_list(); } ##################################################################### @@ -1191,9 +1317,9 @@ sub bz_table_list { # Returns: An array of column names. # sub bz_table_columns_real { - my ($self, $table) = @_; - my $sth = $self->column_info(undef, undef, $table, '%'); - return @{ $self->selectcol_arrayref($sth, {Columns => [4]}) }; + my ($self, $table) = @_; + my $sth = $self->column_info(undef, undef, $table, '%'); + return @{$self->selectcol_arrayref($sth, {Columns => [4]})}; } # bz_table_list_real() @@ -1203,9 +1329,9 @@ sub bz_table_columns_real { # Params: none # Returns: An array containing table names. sub bz_table_list_real { - my ($self) = @_; - my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); - return @{$self->selectcol_arrayref($table_sth, { Columns => [3] })}; + my ($self) = @_; + my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); + return @{$self->selectcol_arrayref($table_sth, {Columns => [3]})}; } ##################################################################### @@ -1213,98 +1339,149 @@ sub bz_table_list_real { ##################################################################### sub bz_in_transaction { - return $_[0]->{private_bz_transaction_count} ? 1 : 0; + return $_[0]->{private_bz_transaction_count} ? 1 : 0; } +## REDHAT EXTENSION 1235135 +# Add unsafe so we can allow some inconsequential updates to go through. sub bz_start_transaction { - my ($self) = @_; - - if ($self->bz_in_transaction) { - $self->{private_bz_transaction_count}++; - } else { - # Turn AutoCommit off and start a new transaction - $self->begin_work(); - # REPEATABLE READ means "We work on a snapshot of the DB that - # is created when we execute our first SQL statement." It's - # what we need in Bugzilla to be safe, for what we do. - # Different DBs have different defaults for their isolation - # level, so we just set it here manually. - if ($self->ISOLATION_LEVEL) { - $self->do('SET TRANSACTION ISOLATION LEVEL ' - . $self->ISOLATION_LEVEL); - } - $self->{private_bz_transaction_count} = 1; + my ($self, $unsafe) = @_; + + if ($self->bz_in_transaction) { + $self->{private_bz_transaction_count}++; + } + else { + # Turn AutoCommit off and start a new transaction + $self->begin_work(); + + # REPEATABLE READ means "We work on a snapshot of the DB that + # is created when we execute our first SQL statement." It's + # what we need in Bugzilla to be safe, for what we do. + # Different DBs have different defaults for their isolation + # level, so we just set it here manually. +## REDHAT EXTENSION START 1235135 + if ($unsafe && $self->ISOLATION_LEVEL) { + $self->do('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + } + elsif ($self->ISOLATION_LEVEL) { +## REDHAT EXTENSION END 1235135 + $self->do('SET TRANSACTION ISOLATION LEVEL ' . $self->ISOLATION_LEVEL); } + $self->{private_bz_transaction_count} = 1; + } } sub bz_commit_transaction { - my ($self) = @_; - - if ($self->{private_bz_transaction_count} > 1) { - $self->{private_bz_transaction_count}--; - } elsif ($self->bz_in_transaction) { - $self->commit(); - $self->{private_bz_transaction_count} = 0; - Bugzilla::Mailer->send_staged_mail(); - } else { - ThrowCodeError('not_in_transaction'); - } + my ($self) = @_; + + if ($self->{private_bz_transaction_count} > 1) { + $self->{private_bz_transaction_count}--; + } + elsif ($self->bz_in_transaction) { + $self->commit(); + $self->{private_bz_transaction_count} = 0; + Bugzilla::Mailer->send_staged_mail(); + } + else { + ThrowCodeError('not_in_transaction'); + } } sub bz_rollback_transaction { - my ($self) = @_; - - # Unlike start and commit, if we rollback at any point it happens - # instantly, even if we're in a nested transaction. - if (!$self->bz_in_transaction) { - ThrowCodeError("not_in_transaction"); - } else { - $self->rollback(); - $self->{private_bz_transaction_count} = 0; - } -} + my ($self) = @_; + + # Unlike start and commit, if we rollback at any point it happens + # instantly, even if we're in a nested transaction. + if (!$self->bz_in_transaction) { + ThrowCodeError("not_in_transaction"); + } + else { + $self->rollback(); + $self->{private_bz_transaction_count} = 0; + } +} + +## REDHAT EXTENSION START 590893 +sub bz_call_with_timeout { + my $self = shift; + my $sth = shift; + my @args = @_; + + ## REDHAT EXTENSION START 967692 + # If we are running from a script, don't do the timeout + my $script = basename($0); + if (remote_ip() eq '127.0.0.1' && $script ne 'whine.pl') { + return $sth->execute(@args); + } + ## REDHAT EXTENSION END 967692 + + ## REDHAT EXTENSION START 966505 + # Since bugzilla can now do multiple queries at once, we need to + # set the timeout to the remaining time. + my $start_time = Bugzilla->request_cache->{_rh_start_time} || $^T; + my $timeout = Bugzilla->params->{'long_query_timeout'} - time + $start_time; + return undef if $timeout <= 0; + ## REDHAT EXTENSION END 966505 + + ## REDHAT EXTENSION BEGIN 1250304 + $self->do("SET STATEMENT_TIMEOUT TO " . ($timeout * 1000)); + my $OK = $sth->execute(@args); + $self->do('SET STATEMENT_TIMEOUT TO 0'); + return ($OK); + ## REDHAT EXTENSION END 1250304 +} +## REDHAT EXTENSION END 590893 ##################################################################### # Subclass Helpers ##################################################################### sub db_new { - my ($class, $params) = @_; - my ($dsn, $user, $pass, $override_attrs) = - @$params{qw(dsn user pass attrs)}; - - # set up default attributes used to connect to the database - # (may be overridden by DB driver implementations) - my $attributes = { RaiseError => 0, - AutoCommit => 1, - PrintError => 0, - ShowErrorStatement => 1, - HandleError => \&_handle_error, - TaintIn => 1, - # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 - # for the reason to use NAME instead of NAME_lc (bug 253696). - FetchHashKeyName => 'NAME', - }; - - if ($override_attrs) { - foreach my $key (keys %$override_attrs) { - $attributes->{$key} = $override_attrs->{$key}; - } + my ($class, $params) = @_; + my ($dsn, $user, $pass, $override_attrs) = @$params{qw(dsn user pass attrs)}; + + # set up default attributes used to connect to the database + # (may be overridden by DB driver implementations) + my $attributes = { + RaiseError => 0, + AutoCommit => 1, + PrintError => 0, + ShowErrorStatement => 1, + HandleError => \&_handle_error, + TaintIn => 1, + + # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 + # for the reason to use NAME instead of NAME_lc (bug 253696). + FetchHashKeyName => 'NAME', + }; + + if ($override_attrs) { + foreach my $key (keys %$override_attrs) { + $attributes->{$key} = $override_attrs->{$key}; } + } + + # connect using our known info to the specified db + my $self = undef; + eval { $self = DBI->connect($dsn, $user, $pass, $attributes); }; + ## REDHAT EXTENSION BEGIN 853262 1575809 + if (!defined($self) || $@) { - # connect using our known info to the specified db - my $self = DBI->connect($dsn, $user, $pass, $attributes) - or die "\nCan't connect to the database.\nError: $DBI::errstr\n" - . " Is your database installed and up and running?\n Do you have" - . " the correct username and password selected in localconfig?\n\n"; + # Display an error to the page for the users. + die + "There was an error connecting to the database. The system administrator has been notified."; + } + ## REDHAT EXTENSION END 853262 1575809 - # RaiseError was only set to 0 so that we could catch the - # above "die" condition. - $self->{RaiseError} = 1; + # RaiseError was only set to 0 so that we could catch the + # above "die" condition. + $self->{RaiseError} = 1; - bless ($self, $class); + bless($self, $class); - return $self; + Bugzilla->request_cache->{in_error} = undef; + + return $self; } ##################################################################### @@ -1328,55 +1505,54 @@ These methods really are private. Do not override them in subclasses. =cut sub _bz_init_schema_storage { - my ($self) = @_; - - my $table_size; - eval { - $table_size = - $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - }; + my ($self) = @_; + + my $table_size; + eval { $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); }; + + if (!$table_size) { + my $init_schema = $self->_bz_get_initial_schema; + my $store_me = $init_schema->serialize_abstract(); + my $schema_version = $init_schema->SCHEMA_VERSION; + + # If table_size is not defined, then we hit an error reading the + # bz_schema table, which means it probably doesn't exist yet. So, + # we have to create it. If we failed above for some other reason, + # we'll see the failure here. + # However, we must create the table after we do get_initial_schema, + # because some versions of get_initial_schema read that the table + # exists and then add it to the Schema, where other versions don't. + if (!defined $table_size) { + $self->_bz_add_table_raw('bz_schema'); + } - if (!$table_size) { - my $init_schema = $self->_bz_get_initial_schema; - my $store_me = $init_schema->serialize_abstract(); - my $schema_version = $init_schema->SCHEMA_VERSION; - - # If table_size is not defined, then we hit an error reading the - # bz_schema table, which means it probably doesn't exist yet. So, - # we have to create it. If we failed above for some other reason, - # we'll see the failure here. - # However, we must create the table after we do get_initial_schema, - # because some versions of get_initial_schema read that the table - # exists and then add it to the Schema, where other versions don't. - if (!defined $table_size) { - $self->_bz_add_table_raw('bz_schema'); - } + say install_string('db_schema_init'); + my $sth = $self->prepare( + "INSERT INTO bz_schema " . " (schema_data, version) VALUES (?,?)"); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); - say install_string('db_schema_init'); - my $sth = $self->prepare("INSERT INTO bz_schema " - ." (schema_data, version) VALUES (?,?)"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); - - # And now we have to update the on-disk schema to hold the bz_schema - # table, if the bz_schema table didn't exist when we were called. - if (!defined $table_size) { - $self->_bz_real_schema->add_table('bz_schema', - $self->_bz_schema->get_table_abstract('bz_schema')); - $self->_bz_store_real_schema; - } - } - # Sanity check - elsif ($table_size > 1) { - # We tell them to delete the newer one. Better to have checksetup - # run migration code too many times than to have it not run the - # correct migration code at all. - die "Attempted to initialize the schema but there are already " - . " $table_size copies of it stored.\nThis should never happen.\n" - . " Compare the rows of the bz_schema table and delete the " - . "newer one(s)."; + # And now we have to update the on-disk schema to hold the bz_schema + # table, if the bz_schema table didn't exist when we were called. + if (!defined $table_size) { + $self->_bz_real_schema->add_table('bz_schema', + $self->_bz_schema->get_table_abstract('bz_schema')); + $self->_bz_store_real_schema; } + } + + # Sanity check + elsif ($table_size > 1) { + + # We tell them to delete the newer one. Better to have checksetup + # run migration code too many times than to have it not run the + # correct migration code at all. + die "Attempted to initialize the schema but there are already " + . " $table_size copies of it stored.\nThis should never happen.\n" + . " Compare the rows of the bz_schema table and delete the " + . "newer one(s)."; + } } =item C<_bz_real_schema()> @@ -1390,24 +1566,23 @@ sub _bz_init_schema_storage { =cut sub _bz_real_schema { - my ($self) = @_; - return $self->{private_real_schema} if exists $self->{private_real_schema}; - - my $bz_schema; - unless ($bz_schema = Bugzilla->memcached->get({ key => 'bz_schema' })) { - $bz_schema = $self->selectrow_arrayref( - "SELECT schema_data, version FROM bz_schema" - ); - Bugzilla->memcached->set({ key => 'bz_schema', value => $bz_schema }); - } + my ($self) = @_; + return $self->{private_real_schema} if exists $self->{private_real_schema}; - (die "_bz_real_schema tried to read the bz_schema table but it's empty!") - if !$bz_schema; + my $bz_schema; + unless ($bz_schema = Bugzilla->memcached->get({key => 'bz_schema'})) { + $bz_schema + = $self->selectrow_arrayref("SELECT schema_data, version FROM bz_schema"); + Bugzilla->memcached->set({key => 'bz_schema', value => $bz_schema}); + } - $self->{private_real_schema} = - $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + (die "_bz_real_schema tried to read the bz_schema table but it's empty!") + if !$bz_schema; - return $self->{private_real_schema}; + $self->{private_real_schema} + = $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + + return $self->{private_real_schema}; } =item C<_bz_store_real_schema()> @@ -1427,106 +1602,135 @@ sub _bz_real_schema { =cut sub _bz_store_real_schema { - my ($self) = @_; - - # Make sure that there's a schema to update - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - - die "Attempted to update the bz_schema table but there's nothing " - . "there to update. Run checksetup." unless $table_size; - - # We want to store the current object, not one - # that we read from the database. So we use the actual hash - # member instead of the subroutine call. If the hash - # member is not defined, we will (and should) fail. - my $update_schema = $self->{private_real_schema}; - my $store_me = $update_schema->serialize_abstract(); - my $schema_version = $update_schema->SCHEMA_VERSION; - my $sth = $self->prepare("UPDATE bz_schema - SET schema_data = ?, version = ?"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); + my ($self) = @_; + + # Make sure that there's a schema to update + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - Bugzilla->memcached->clear({ key => 'bz_schema' }); + die "Attempted to update the bz_schema table but there's nothing " + . "there to update. Run checksetup." + unless $table_size; + + # We want to store the current object, not one + # that we read from the database. So we use the actual hash + # member instead of the subroutine call. If the hash + # member is not defined, we will (and should) fail. + my $update_schema = $self->{private_real_schema}; + my $store_me = $update_schema->serialize_abstract(); + my $schema_version = $update_schema->SCHEMA_VERSION; + my $sth = $self->prepare( + "UPDATE bz_schema + SET schema_data = ?, version = ?" + ); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); + + Bugzilla->memcached->clear({key => 'bz_schema'}); } # For bz_populate_enum_tables sub _bz_populate_enum_table { - my ($self, $table, $valuelist) = @_; - - my $sql_table = $self->quote_identifier($table); - - # Check if there are any table entries - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); - - # If the table is empty... - if (!$table_size) { - print " $table"; - my $insert = $self->prepare( - "INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); - my $sortorder = 0; - my $maxlen = max(map(length($_), @$valuelist)) + 2; - foreach my $value (@$valuelist) { - $sortorder += 100; - $insert->execute($value, $sortorder); - } + my ($self, $table, $valuelist) = @_; + + my $sql_table = $self->quote_identifier($table); + + # Check if there are any table entries + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); + + # If the table is empty... + if (!$table_size) { + print " $table"; + my $insert + = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); + my $sortorder = 0; + my $maxlen = max(map(length($_), @$valuelist)) + 2; + foreach my $value (@$valuelist) { + $sortorder += 100; + $insert->execute($value, $sortorder); } + } } # This is used before adding a foreign key to a column, to make sure # that the database won't fail adding the key. sub _check_references { - my ($self, $table, $column, $fk) = @_; - my $foreign_table = $fk->{TABLE}; - my $foreign_column = $fk->{COLUMN}; - - # We use table aliases because sometimes we join a table to itself, - # and we can't use the same table name on both sides of the join. - # We also can't use the words "table" or "foreign" because those are - # reserved words. - my $bad_values = $self->selectcol_arrayref( - "SELECT DISTINCT tabl.$column + my ($self, $table, $column, $fk) = @_; + my $foreign_table = $fk->{TABLE}; + my $foreign_column = $fk->{COLUMN}; + + # We use table aliases because sometimes we join a table to itself, + # and we can't use the same table name on both sides of the join. + # We also can't use the words "table" or "foreign" because those are + # reserved words. + my $bad_values = $self->selectcol_arrayref( + "SELECT DISTINCT tabl.$column FROM $table AS tabl LEFT JOIN $foreign_table AS forn ON tabl.$column = forn.$foreign_column WHERE forn.$foreign_column IS NULL - AND tabl.$column IS NOT NULL"); - - if (@$bad_values) { - my $delete_action = $fk->{DELETE} || ''; - if ($delete_action eq 'CASCADE') { - $self->do("DELETE FROM $table WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'delete' }), "\n"; - } - } - elsif ($delete_action eq 'SET NULL') { - $self->do("UPDATE $table SET $column = NULL + AND tabl.$column IS NOT NULL" + ); + + if (@$bad_values) { + my $delete_action = $fk->{DELETE} || ''; + if ($delete_action eq 'CASCADE') { + $self->do( + "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")", + undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'delete' + } + ), + "\n"; + } + } + elsif ($delete_action eq 'SET NULL') { + $self->do( + "UPDATE $table SET $column = NULL WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'null' }), "\n"; - } - } - else { - die "\n", get_text('install_fk_invalid', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values }), "\n"; + . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'null' + } + ), + "\n"; + } + } + else { + die "\n", + get_text( + 'install_fk_invalid', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values } + ), + "\n"; } + } } 1; @@ -2847,4 +3051,6 @@ L<Bugzilla::Constants/DB_MODULE> =item bz_add_field_tables +=item bz_call_with_timeout + =back |