diff options
author | 2020-01-29 06:50:00 -0800 | |
---|---|---|
committer | 2020-01-29 06:56:28 -0800 | |
commit | 976f4aa6d3e82e568149dd58a0197b36641b7b81 (patch) | |
tree | 5df93d3bd42ed5eebfce726bdf559d45d2684075 /Bugzilla/DB | |
parent | Merge remote-tracking branch 'origin/master' into bugstest (diff) | |
parent | Merge tag 'release-5.0.6' into bugstest (diff) | |
download | bugzilla-976f4aa6d3e82e568149dd58a0197b36641b7b81.tar.gz bugzilla-976f4aa6d3e82e568149dd58a0197b36641b7b81.tar.bz2 bugzilla-976f4aa6d3e82e568149dd58a0197b36641b7b81.zip |
Merge branch 'bugstest-5.0.6' into bugstest
Merge my 5.0.6 import changes. This is specifically a merge to make it
easier to merge upstream changes again after the code reformatting.
Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>
Diffstat (limited to 'Bugzilla/DB')
-rw-r--r-- | Bugzilla/DB/Mysql.pm | 1603 | ||||
-rw-r--r-- | Bugzilla/DB/Oracle.pm | 1019 | ||||
-rw-r--r-- | Bugzilla/DB/Pg.pm | 611 | ||||
-rw-r--r-- | Bugzilla/DB/Schema.pm | 4123 | ||||
-rw-r--r-- | Bugzilla/DB/Schema/Mysql.pm | 577 | ||||
-rw-r--r-- | Bugzilla/DB/Schema/Oracle.pm | 782 | ||||
-rw-r--r-- | Bugzilla/DB/Schema/Pg.pm | 286 | ||||
-rw-r--r-- | Bugzilla/DB/Schema/Sqlite.pm | 420 | ||||
-rw-r--r-- | Bugzilla/DB/Sqlite.pm | 324 |
9 files changed, 4958 insertions, 4787 deletions
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm index d0915f1e6..a58d88df4 100644 --- a/Bugzilla/DB/Mysql.pm +++ b/Bugzilla/DB/Mysql.pm @@ -37,258 +37,265 @@ use List::Util qw(max); use Text::ParseWords; # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug. -# In reality, you could have a LOT more comments than this, because +# In reality, you could have a LOT more comments than this, because # MAX_COMMENT_LENGTH is big. use constant MAX_COMMENTS => 50; use constant FULLTEXT_OR => '|'; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port, $sock) = - @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:mysql:host=$host;database=$dbname"; - $dsn .= ";port=$port" if $port; - $dsn .= ";mysql_socket=$sock" if $sock; - - my %attrs = ( - mysql_enable_utf8 => Bugzilla->params->{'utf8'}, - # Needs to be explicitly specified for command-line processes. - mysql_auto_reconnect => 1, - ); - - # MySQL SSL options - my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = - @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path - db_mysql_ssl_client_cert db_mysql_ssl_client_key)}; - if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { - $attrs{'mysql_ssl'} = 1; - $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; - $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; - $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; - $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port, $sock) + = @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:mysql:host=$host;database=$dbname"; + $dsn .= ";port=$port" if $port; + $dsn .= ";mysql_socket=$sock" if $sock; + + my %attrs = ( + mysql_enable_utf8 => Bugzilla->params->{'utf8'}, + + # Needs to be explicitly specified for command-line processes. + mysql_auto_reconnect => 1, + ); + + # MySQL SSL options + my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = @$params{ + qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path + db_mysql_ssl_client_cert db_mysql_ssl_client_key) + }; + if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { + $attrs{'mysql_ssl'} = 1; + $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; + $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; + $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; + $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + } + + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs}); + + # This makes sure that if the tables are encoded as UTF-8, we + # return their data correctly. + $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; + + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Check for MySQL modes. + my ($var, $sql_mode) + = $self->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'"); + + # Disable ANSI and strict modes, else Bugzilla will crash. + if ($sql_mode) { + + # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode, + # causing bug 321645. TRADITIONAL sets these modes (among others) as + # well, so it has to be stipped as well + my $new_sql_mode = join(",", + grep { $_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/ } + split(/,/, $sql_mode)); + + if ($sql_mode ne $new_sql_mode) { + $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); } + } - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => \%attrs }); - - # This makes sure that if the tables are encoded as UTF-8, we - # return their data correctly. - $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; - - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # Allow large GROUP_CONCATs (largely for inserting comments + # into bugs_fulltext). + $self->do('SET SESSION group_concat_max_len = 128000000'); - bless ($self, $class); + # MySQL 5.5.2 and older have this variable set to true, which causes + # trouble, see bug 870369. + $self->do('SET SESSION sql_auto_is_null = 0'); - # Check for MySQL modes. - my ($var, $sql_mode) = $self->selectrow_array( - "SHOW VARIABLES LIKE 'sql\\_mode'"); - - # Disable ANSI and strict modes, else Bugzilla will crash. - if ($sql_mode) { - # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode, - # causing bug 321645. TRADITIONAL sets these modes (among others) as - # well, so it has to be stipped as well - my $new_sql_mode = - join(",", grep {$_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/} - split(/,/, $sql_mode)); - - if ($sql_mode ne $new_sql_mode) { - $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); - } - } - - # Allow large GROUP_CONCATs (largely for inserting comments - # into bugs_fulltext). - $self->do('SET SESSION group_concat_max_len = 128000000'); - - # MySQL 5.5.2 and older have this variable set to true, which causes - # trouble, see bug 870369. - $self->do('SET SESSION sql_auto_is_null = 0'); - - return $self; + return $self; } # when last_insert_id() is supported on MySQL by lowest DBI/DBD version # required by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self) = @_; + my ($self) = @_; - my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); + my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $column, $separator, $sort, $order_by) = @_; - $separator = $self->quote(', ') if !defined $separator; - $sort = 1 if !defined $sort; - if ($order_by) { - $column .= " ORDER BY $order_by"; - } - elsif ($sort) { - my $sort_order = $column; - $sort_order =~ s/^DISTINCT\s+//i; - $column = "$column ORDER BY $sort_order"; - } - return "GROUP_CONCAT($column SEPARATOR $separator)"; + my ($self, $column, $separator, $sort, $order_by) = @_; + $separator = $self->quote(', ') if !defined $separator; + $sort = 1 if !defined $sort; + if ($order_by) { + $column .= " ORDER BY $order_by"; + } + elsif ($sort) { + my $sort_order = $column; + $sort_order =~ s/^DISTINCT\s+//i; + $column = "$column ORDER BY $sort_order"; + } + return "GROUP_CONCAT($column SEPARATOR $separator)"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr NOT REGEXP $pattern"; + return "$expr NOT REGEXP $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $offset, $limit"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $offset, $limit"; + } + else { + return "LIMIT $limit"; + } } sub sql_string_concat { - my ($self, @params) = @_; - - return 'CONCAT(' . join(', ', @params) . ')'; + my ($self, @params) = @_; + + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # Add the boolean mode modifier if the search string contains - # boolean operators at the start or end of a word. - my $mode = ''; - if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { - $mode = 'IN BOOLEAN MODE'; - - my @terms = split(quotemeta(FULLTEXT_OR), $text); - foreach my $term (@terms) { - # quote un-quoted compound words - my @words = quotewords('[\s()]+', 'delimiters', $term); - foreach my $word (@words) { - # match words that have non-word chars in the middle of them - if ($word =~ /\w\W+\w/ && $word !~ m/"/) { - $word = '"' . $word . '"'; - } - } - $term = join('', @words); + my ($self, $column, $text) = @_; + + # Add the boolean mode modifier if the search string contains + # boolean operators at the start or end of a word. + my $mode = ''; + if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { + $mode = 'IN BOOLEAN MODE'; + + my @terms = split(quotemeta(FULLTEXT_OR), $text); + foreach my $term (@terms) { + + # quote un-quoted compound words + my @words = quotewords('[\s()]+', 'delimiters', $term); + foreach my $word (@words) { + + # match words that have non-word chars in the middle of them + if ($word =~ /\w\W+\w/ && $word !~ m/"/) { + $word = '"' . $word . '"'; } - $text = join(FULLTEXT_OR, @terms); + } + $term = join('', @words); } + $text = join(FULLTEXT_OR, @terms); + } - # quote the text for use in the MATCH AGAINST expression - $text = $self->quote($text); + # quote the text for use in the MATCH AGAINST expression + $text = $self->quote($text); - # untaint the text, since it's safe to use now that we've quoted it - trick_taint($text); + # untaint the text, since it's safe to use now that we've quoted it + trick_taint($text); - return "MATCH($column) AGAINST($text $mode)"; + return "MATCH($column) AGAINST($text $mode)"; } sub sql_istring { - my ($self, $string) = @_; - - return $string; + my ($self, $string) = @_; + + return $string; } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "FROM_DAYS($days)"; + return "FROM_DAYS($days)"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_DAYS($date)"; + return "TO_DAYS($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - return "DATE_FORMAT($date, " . $self->quote($format) . ")"; + return "DATE_FORMAT($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator INTERVAL $interval $units"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator INTERVAL $interval $units"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; + return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; - # MySQL allows you to specify the minimal subset of columns to get - # a unique result. While it does allow specifying all columns as - # ANSI SQL requires, according to MySQL documentation, the fewer - # columns you specify, the faster the query runs. - return "GROUP BY $needed_columns"; + # MySQL allows you to specify the minimal subset of columns to get + # a unique result. While it does allow specifying all columns as + # ANSI SQL requires, according to MySQL documentation, the fewer + # columns you specify, the faster the query runs. + return "GROUP BY $needed_columns"; } sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN $sql"); - $sth->execute(); - my $columns = $sth->{'NAME'}; - my $lengths = $sth->{'mysql_max_length'}; - my $format_string = '|'; - my $i = 0; - foreach my $column (@$columns) { - # Sometimes the column name is longer than the contents. - my $length = max($lengths->[$i], length($column)); - $format_string .= ' %-' . $length . 's |'; - $i++; - } - - my $first_row = sprintf($format_string, @$columns); - my @explain_rows = ($first_row, '-' x length($first_row)); - while (my $row = $sth->fetchrow_arrayref) { - my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; - push(@explain_rows, sprintf($format_string, @fixed)); - } - - return join("\n", @explain_rows); + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN $sql"); + $sth->execute(); + my $columns = $sth->{'NAME'}; + my $lengths = $sth->{'mysql_max_length'}; + my $format_string = '|'; + my $i = 0; + foreach my $column (@$columns) { + + # Sometimes the column name is longer than the contents. + my $length = max($lengths->[$i], length($column)); + $format_string .= ' %-' . $length . 's |'; + $i++; + } + + my $first_row = sprintf($format_string, @$columns); + my @explain_rows = ($first_row, '-' x length($first_row)); + while (my $row = $sth->fetchrow_arrayref) { + my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; + push(@explain_rows, sprintf($format_string, @fixed)); + } + + return join("\n", @explain_rows); } sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_build_schema_from_disk(); + my ($self) = @_; + return $self->_bz_build_schema_from_disk(); } ##################################################################### @@ -296,493 +303,503 @@ sub _bz_get_initial_schema { ##################################################################### sub bz_check_server_version { - my $self = shift; + my $self = shift; - my $lc = Bugzilla->localconfig; - if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { - die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" - . " Please pick a different value for \$db_name in localconfig.\n"; - } + my $lc = Bugzilla->localconfig; + if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { + die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" + . " Please pick a different value for \$db_name in localconfig.\n"; + } - $self->SUPER::bz_check_server_version(@_); + $self->SUPER::bz_check_server_version(@_); } sub bz_setup_database { - my ($self) = @_; - - # The "comments" field of the bugs_fulltext table could easily exceed - # MySQL's default max_allowed_packet. Also, MySQL should never have - # a max_allowed_packet smaller than our max_attachment_size. So, we - # warn the user here if max_allowed_packet is too small. - my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; - my (undef, $current_max_allowed) = $self->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - # This parameter is not yet defined when the DB is being built for - # the very first time. The code below still works properly, however, - # because the default maxattachmentsize is smaller than $min_max_allowed. - my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; - my $needed_max_allowed = max($min_max_allowed, $max_attachment); - if ($current_max_allowed < $needed_max_allowed) { - warn install_string('max_allowed_packet', - { current => $current_max_allowed, - needed => $needed_max_allowed }) . "\n"; + my ($self) = @_; + + # The "comments" field of the bugs_fulltext table could easily exceed + # MySQL's default max_allowed_packet. Also, MySQL should never have + # a max_allowed_packet smaller than our max_attachment_size. So, we + # warn the user here if max_allowed_packet is too small. + my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; + my (undef, $current_max_allowed) + = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + + # This parameter is not yet defined when the DB is being built for + # the very first time. The code below still works properly, however, + # because the default maxattachmentsize is smaller than $min_max_allowed. + my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; + my $needed_max_allowed = max($min_max_allowed, $max_attachment); + if ($current_max_allowed < $needed_max_allowed) { + warn install_string('max_allowed_packet', + {current => $current_max_allowed, needed => $needed_max_allowed}) + . "\n"; + } + + # Make sure the installation has InnoDB turned on, or we're going to be + # doing silly things like making foreign keys on MyISAM tables, which is + # hard to fix later. We do this up here because none of the code below + # works if InnoDB is off. (Particularly if we've already converted the + # tables to InnoDB.) + my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})}; + if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { + die install_string('mysql_innodb_disabled'); + } + + + my ($sd_index_deleted, $longdescs_index_deleted); + my @tables = $self->bz_table_list_real(); + + # We want to convert tables to InnoDB, but it's possible that they have + # fulltext indexes on them, and conversion will fail unless we remove + # the indexes. + if (grep($_ eq 'bugs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) { + if ($self->bz_index_info_real('bugs', 'short_desc')) { + $self->bz_drop_index_raw('bugs', 'short_desc'); } - - # Make sure the installation has InnoDB turned on, or we're going to be - # doing silly things like making foreign keys on MyISAM tables, which is - # hard to fix later. We do this up here because none of the code below - # works if InnoDB is off. (Particularly if we've already converted the - # tables to InnoDB.) - my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1,2]})}; - if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { - die install_string('mysql_innodb_disabled'); + if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { + $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); + $sd_index_deleted = 1; # Used for later schema cleanup. } - - - my ($sd_index_deleted, $longdescs_index_deleted); - my @tables = $self->bz_table_list_real(); - # We want to convert tables to InnoDB, but it's possible that they have - # fulltext indexes on them, and conversion will fail unless we remove - # the indexes. - if (grep($_ eq 'bugs', @tables) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('bugs', 'short_desc')) { - $self->bz_drop_index_raw('bugs', 'short_desc'); - } - if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { - $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); - $sd_index_deleted = 1; # Used for later schema cleanup. - } + } + if (grep($_ eq 'longdescs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) + { + if ($self->bz_index_info_real('longdescs', 'thetext')) { + $self->bz_drop_index_raw('longdescs', 'thetext'); } - if (grep($_ eq 'longdescs', @tables) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('longdescs', 'thetext')) { - $self->bz_drop_index_raw('longdescs', 'thetext'); - } - if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { - $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); - $longdescs_index_deleted = 1; # For later schema cleanup. - } + if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { + $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); + $longdescs_index_deleted = 1; # For later schema cleanup. } - - # Upgrade tables from MyISAM to InnoDB - my $db_name = Bugzilla->localconfig->{db_name}; - my $myisam_tables = $self->selectcol_arrayref( - 'SELECT TABLE_NAME FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND ENGINE = ?', - undef, $db_name, 'MyISAM'); - foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { - @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + # Upgrade tables from MyISAM to InnoDB + my $db_name = Bugzilla->localconfig->{db_name}; + my $myisam_tables = $self->selectcol_arrayref( + 'SELECT TABLE_NAME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM' + ); + foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { + @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + if (scalar @$myisam_tables) { + print "Bugzilla now uses the InnoDB storage engine in MySQL for", + " most tables.\nConverting tables to InnoDB:\n"; + foreach my $table (@$myisam_tables) { + print "Converting table $table... "; + $self->do("ALTER TABLE $table ENGINE = InnoDB"); + print "done.\n"; } - - if (scalar @$myisam_tables) { - print "Bugzilla now uses the InnoDB storage engine in MySQL for", - " most tables.\nConverting tables to InnoDB:\n"; - foreach my $table (@$myisam_tables) { - print "Converting table $table... "; - $self->do("ALTER TABLE $table ENGINE = InnoDB"); - print "done.\n"; - } + } + + # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did + # not provide explicit names for the table indexes. This means + # that our upgrades will not be reliable, because we look for the name + # of the index, not what fields it is on, when doing upgrades. + # (using the name is much better for cross-database compatibility + # and general reliability). It's also very important that our + # Schema object be consistent with what is on the disk. + # + # While we're at it, we also fix some inconsistent index naming + # from the original checkin of Bugzilla::DB::Schema. + + # We check for the existence of a particular "short name" index that + # has existed at least since Bugzilla 2.8, and probably earlier. + # For fixing the inconsistent naming of Schema indexes, + # we also check for one of those inconsistently-named indexes. + if ( + grep($_ eq 'bugs', @tables) + && ( $self->bz_index_info_real('bugs', 'assigned_to') + || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) + ) + { + + # This is a check unrelated to the indexes, to see if people are + # upgrading from 2.18 or below, but somehow have a bz_schema table + # already. This only happens if they have done a mysqldump into + # a database without doing a DROP DATABASE first. + # We just do the check here since this check is a reliable way + # of telling that we are upgrading from a version pre-2.20. + if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { + die install_string('bz_schema_exists_before_220'); } - - # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did - # not provide explicit names for the table indexes. This means - # that our upgrades will not be reliable, because we look for the name - # of the index, not what fields it is on, when doing upgrades. - # (using the name is much better for cross-database compatibility - # and general reliability). It's also very important that our - # Schema object be consistent with what is on the disk. - # - # While we're at it, we also fix some inconsistent index naming - # from the original checkin of Bugzilla::DB::Schema. - - # We check for the existence of a particular "short name" index that - # has existed at least since Bugzilla 2.8, and probably earlier. - # For fixing the inconsistent naming of Schema indexes, - # we also check for one of those inconsistently-named indexes. - if (grep($_ eq 'bugs', @tables) - && ($self->bz_index_info_real('bugs', 'assigned_to') - || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) ) - { - # This is a check unrelated to the indexes, to see if people are - # upgrading from 2.18 or below, but somehow have a bz_schema table - # already. This only happens if they have done a mysqldump into - # a database without doing a DROP DATABASE first. - # We just do the check here since this check is a reliable way - # of telling that we are upgrading from a version pre-2.20. - if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { - die install_string('bz_schema_exists_before_220'); - } + my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - # We estimate one minute for each 3000 bugs, plus 3 minutes just - # to handle basic MySQL stuff. - my $rename_time = int($bug_count / 3000) + 3; - # And 45 minutes for every 15,000 attachments, per some experiments. - my ($attachment_count) = - $self->selectrow_array("SELECT COUNT(*) FROM attachments"); - $rename_time += int(($attachment_count * 45) / 15000); - # If we're going to take longer than 5 minutes, we let the user know - # and allow them to abort. - if ($rename_time > 5) { - print "\n", install_string('mysql_index_renaming', - { minutes => $rename_time }); - # Wait 45 seconds for them to respond. - sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; - } - print "Renaming indexes...\n"; - - # We can't be interrupted, because of how the "if" - # works above. - local $SIG{INT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - - # Certain indexes had names in Schema that did not easily conform - # to a standard. We store those names here, so that they - # can be properly renamed. - # Also, sometimes an old mysqldump would incorrectly rename - # unique indexes to "PRIMARY", so we address that here, also. - my $bad_names = { - # 'when' is a possible leftover from Bugzillas before 2.8 - bugs_activity => ['when', 'bugs_activity_bugid_idx', - 'bugs_activity_bugwhen_idx'], - cc => ['PRIMARY'], - longdescs => ['longdescs_bugid_idx', - 'longdescs_bugwhen_idx'], - flags => ['flags_bidattid_idx'], - flaginclusions => ['flaginclusions_tpcid_idx'], - flagexclusions => ['flagexclusions_tpc_id_idx'], - keywords => ['PRIMARY'], - milestones => ['PRIMARY'], - profiles_activity => ['profiles_activity_when_idx'], - group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], - user_group_map => ['PRIMARY'], - group_group_map => ['PRIMARY'], - email_setting => ['PRIMARY'], - bug_group_map => ['PRIMARY'], - category_group_map => ['PRIMARY'], - watch => ['PRIMARY'], - namedqueries => ['PRIMARY'], - series_data => ['PRIMARY'], - # series_categories is dealt with below, not here. - }; - - # The series table is broken and needs to have one index - # dropped before we begin the renaming, because it had a - # useless index on it that would cause a naming conflict here. - if (grep($_ eq 'series', @tables)) { - my $dropname; - # This is what the bad index was called before Schema. - if ($self->bz_index_info_real('series', 'creator_2')) { - $dropname = 'creator_2'; - } - # This is what the bad index is called in Schema. - elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { - $dropname = 'series_creator_idx'; - } - $self->bz_drop_index_raw('series', $dropname) if $dropname; - } + # We estimate one minute for each 3000 bugs, plus 3 minutes just + # to handle basic MySQL stuff. + my $rename_time = int($bug_count / 3000) + 3; - # The email_setting table also had the same problem. - if( grep($_ eq 'email_setting', @tables) - && $self->bz_index_info_real('email_setting', - 'email_settings_user_id_idx') ) - { - $self->bz_drop_index_raw('email_setting', - 'email_settings_user_id_idx'); - } - - # Go through all the tables. - foreach my $table (@tables) { - # Will contain the names of old indexes as keys, and the - # definition of the new indexes as a value. The values - # include an extra hash key, NAME, with the new name of - # the index. - my %rename_indexes; - # And go through all the columns on each table. - my @columns = $self->bz_table_columns_real($table); - - # We also want to fix the silly naming of unique indexes - # that happened when we first checked-in Bugzilla::DB::Schema. - if ($table eq 'series_categories') { - # The series_categories index had a nonstandard name. - push(@columns, 'series_cats_unique_idx'); - } - elsif ($table eq 'email_setting') { - # The email_setting table had a similar problem. - push(@columns, 'email_settings_unique_idx'); - } - else { - push(@columns, "${table}_unique_idx"); - } - # And this is how we fix the other inconsistent Schema naming. - push(@columns, @{$bad_names->{$table}}) - if (exists $bad_names->{$table}); - foreach my $column (@columns) { - # If we have an index named after this column, it's an - # old-style-name index. - if (my $index = $self->bz_index_info_real($table, $column)) { - # Fix the name to fit in with the new naming scheme. - $index->{NAME} = $table . "_" . - $index->{FIELDS}->[0] . "_idx"; - print "Renaming index $column to " - . $index->{NAME} . "...\n"; - $rename_indexes{$column} = $index; - } # if - } # foreach column - - my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl( - $table, %rename_indexes); - $self->do($_) foreach (@rename_sql); - - } # foreach table - } # if old-name indexes - - # If there are no tables, but the DB isn't utf8 and it should be, - # then we should alter the database to be utf8. We know it should be - # if the utf8 parameter is true or there are no params at all. - # This kind of situation happens when people create the database - # themselves, and if we don't do this they will get the big - # scary WARNING statement about conversion to UTF8. - if ( !$self->bz_db_is_utf8 && !@tables - && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params}) ) - { - $self->_alter_db_charset_to_utf8(); - } + # And 45 minutes for every 15,000 attachments, per some experiments. + my ($attachment_count) + = $self->selectrow_array("SELECT COUNT(*) FROM attachments"); + $rename_time += int(($attachment_count * 45) / 15000); - # And now we create the tables and the Schema object. - $self->SUPER::bz_setup_database(); + # If we're going to take longer than 5 minutes, we let the user know + # and allow them to abort. + if ($rename_time > 5) { + print "\n", install_string('mysql_index_renaming', {minutes => $rename_time}); - if ($sd_index_deleted) { - $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); - $self->_bz_store_real_schema; + # Wait 45 seconds for them to respond. + sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; } - if ($longdescs_index_deleted) { - $self->_bz_real_schema->delete_index('longdescs', - 'longdescs_thetext_idx'); - $self->_bz_store_real_schema; + print "Renaming indexes...\n"; + + # We can't be interrupted, because of how the "if" + # works above. + local $SIG{INT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + # Certain indexes had names in Schema that did not easily conform + # to a standard. We store those names here, so that they + # can be properly renamed. + # Also, sometimes an old mysqldump would incorrectly rename + # unique indexes to "PRIMARY", so we address that here, also. + my $bad_names = { + + # 'when' is a possible leftover from Bugzillas before 2.8 + bugs_activity => + ['when', 'bugs_activity_bugid_idx', 'bugs_activity_bugwhen_idx'], + cc => ['PRIMARY'], + longdescs => ['longdescs_bugid_idx', 'longdescs_bugwhen_idx'], + flags => ['flags_bidattid_idx'], + flaginclusions => ['flaginclusions_tpcid_idx'], + flagexclusions => ['flagexclusions_tpc_id_idx'], + keywords => ['PRIMARY'], + milestones => ['PRIMARY'], + profiles_activity => ['profiles_activity_when_idx'], + group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], + user_group_map => ['PRIMARY'], + group_group_map => ['PRIMARY'], + email_setting => ['PRIMARY'], + bug_group_map => ['PRIMARY'], + category_group_map => ['PRIMARY'], + watch => ['PRIMARY'], + namedqueries => ['PRIMARY'], + series_data => ['PRIMARY'], + + # series_categories is dealt with below, not here. + }; + + # The series table is broken and needs to have one index + # dropped before we begin the renaming, because it had a + # useless index on it that would cause a naming conflict here. + if (grep($_ eq 'series', @tables)) { + my $dropname; + + # This is what the bad index was called before Schema. + if ($self->bz_index_info_real('series', 'creator_2')) { + $dropname = 'creator_2'; + } + + # This is what the bad index is called in Schema. + elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { + $dropname = 'series_creator_idx'; + } + $self->bz_drop_index_raw('series', $dropname) if $dropname; } - # The old timestamp fields need to be adjusted here instead of in - # checksetup. Otherwise the UPDATE statements inside of bz_add_column - # will cause accidental timestamp updates. - # The code that does this was moved here from checksetup. - - # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 - # attachments creation time needs to be a datetime, not a timestamp - my $attach_creation = - $self->bz_column_info("attachments", "creation_ts"); - if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { - print "Fixing creation time on attachments...\n"; + # The email_setting table also had the same problem. + if (grep($_ eq 'email_setting', @tables) + && $self->bz_index_info_real('email_setting', 'email_settings_user_id_idx')) + { + $self->bz_drop_index_raw('email_setting', 'email_settings_user_id_idx'); + } - my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); - $sth->execute(); - my ($attach_count) = $sth->fetchrow_array(); + # Go through all the tables. + foreach my $table (@tables) { - if ($attach_count > 1000) { - print "This may take a while...\n"; - } - my $i = 0; - - # This isn't just as simple as changing the field type, because - # the creation_ts was previously updated when an attachment was made - # obsolete from the attachment creation screen. So we have to go - # and recreate these times from the comments.. - $sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " . - "FROM attachments"); - $sth->execute(); - - # Restrict this as much as possible in order to avoid false - # positives, and keep the db search time down - my $sth2 = $self->prepare("SELECT bug_when FROM longdescs - WHERE bug_id=? AND who=? - AND thetext LIKE ? - ORDER BY bug_when " . $self->sql_limit(1)); - while (my ($bug_id, $attach_id, $submitter_id) - = $sth->fetchrow_array()) - { - $sth2->execute($bug_id, $submitter_id, - "Created an attachment (id=$attach_id)%"); - my ($when) = $sth2->fetchrow_array(); - if ($when) { - $self->do("UPDATE attachments " . - "SET creation_ts='$when' " . - "WHERE attach_id=$attach_id"); - } else { - print "Warning - could not determine correct creation" - . " time for attachment $attach_id on bug $bug_id\n"; - } - ++$i; - print "Converted $i of $attach_count attachments\n" if !($i % 1000); - } - print "Done - converted $i attachments\n"; + # Will contain the names of old indexes as keys, and the + # definition of the new indexes as a value. The values + # include an extra hash key, NAME, with the new name of + # the index. + my %rename_indexes; + + # And go through all the columns on each table. + my @columns = $self->bz_table_columns_real($table); + + # We also want to fix the silly naming of unique indexes + # that happened when we first checked-in Bugzilla::DB::Schema. + if ($table eq 'series_categories') { + + # The series_categories index had a nonstandard name. + push(@columns, 'series_cats_unique_idx'); + } + elsif ($table eq 'email_setting') { + + # The email_setting table had a similar problem. + push(@columns, 'email_settings_unique_idx'); + } + else { + push(@columns, "${table}_unique_idx"); + } + + # And this is how we fix the other inconsistent Schema naming. + push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table}); + foreach my $column (@columns) { + + # If we have an index named after this column, it's an + # old-style-name index. + if (my $index = $self->bz_index_info_real($table, $column)) { + + # Fix the name to fit in with the new naming scheme. + $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx"; + print "Renaming index $column to " . $index->{NAME} . "...\n"; + $rename_indexes{$column} = $index; + } # if + } # foreach column + + my @rename_sql + = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes); + $self->do($_) foreach (@rename_sql); + + } # foreach table + } # if old-name indexes + + # If there are no tables, but the DB isn't utf8 and it should be, + # then we should alter the database to be utf8. We know it should be + # if the utf8 parameter is true or there are no params at all. + # This kind of situation happens when people create the database + # themselves, and if we don't do this they will get the big + # scary WARNING statement about conversion to UTF8. + if ( !$self->bz_db_is_utf8 + && !@tables + && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params})) + { + $self->_alter_db_charset_to_utf8(); + } + + # And now we create the tables and the Schema object. + $self->SUPER::bz_setup_database(); + + if ($sd_index_deleted) { + $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); + $self->_bz_store_real_schema; + } + if ($longdescs_index_deleted) { + $self->_bz_real_schema->delete_index('longdescs', 'longdescs_thetext_idx'); + $self->_bz_store_real_schema; + } + + # The old timestamp fields need to be adjusted here instead of in + # checksetup. Otherwise the UPDATE statements inside of bz_add_column + # will cause accidental timestamp updates. + # The code that does this was moved here from checksetup. + + # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 + # attachments creation time needs to be a datetime, not a timestamp + my $attach_creation = $self->bz_column_info("attachments", "creation_ts"); + if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { + print "Fixing creation time on attachments...\n"; + + my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); + $sth->execute(); + my ($attach_count) = $sth->fetchrow_array(); - $self->bz_alter_column("attachments", "creation_ts", - {TYPE => 'DATETIME', NOTNULL => 1}); + if ($attach_count > 1000) { + print "This may take a while...\n"; } + my $i = 0; - # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 - # Change logincookies.lastused type from timestamp to datetime - my $login_lastused = $self->bz_column_info("logincookies", "lastused"); - if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('logincookies', 'lastused', - { TYPE => 'DATETIME', NOTNULL => 1}); - } + # This isn't just as simple as changing the field type, because + # the creation_ts was previously updated when an attachment was made + # obsolete from the attachment creation screen. So we have to go + # and recreate these times from the comments.. + $sth = $self->prepare( + "SELECT bug_id, attach_id, submitter_id " . "FROM attachments"); + $sth->execute(); - # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 - # Change bugs.delta_ts type from timestamp to datetime - my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); - if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('bugs', 'delta_ts', - {TYPE => 'DATETIME', NOTNULL => 1}); + # Restrict this as much as possible in order to avoid false + # positives, and keep the db search time down + my $sth2 = $self->prepare( + "SELECT bug_when FROM longdescs + WHERE bug_id=? AND who=? + AND thetext LIKE ? + ORDER BY bug_when " . $self->sql_limit(1) + ); + while (my ($bug_id, $attach_id, $submitter_id) = $sth->fetchrow_array()) { + $sth2->execute($bug_id, $submitter_id, + "Created an attachment (id=$attach_id)%"); + my ($when) = $sth2->fetchrow_array(); + if ($when) { + $self->do("UPDATE attachments " + . "SET creation_ts='$when' " + . "WHERE attach_id=$attach_id"); + } + else { + print "Warning - could not determine correct creation" + . " time for attachment $attach_id on bug $bug_id\n"; + } + ++$i; + print "Converted $i of $attach_count attachments\n" if !($i % 1000); } - - # 2005-09-24 - bugreport@peshkin.net, bug 307602 - # Make sure that default 4G table limit is overridden - my $attach_data_create = $self->selectrow_array( - 'SELECT CREATE_OPTIONS FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', - undef, $db_name, 'attach_data'); - if ($attach_data_create !~ /MAX_ROWS/i) { - print "Converting attach_data maximum size to 100G...\n"; - $self->do("ALTER TABLE attach_data + print "Done - converted $i attachments\n"; + + $self->bz_alter_column("attachments", "creation_ts", + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 + # Change logincookies.lastused type from timestamp to datetime + my $login_lastused = $self->bz_column_info("logincookies", "lastused"); + if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('logincookies', 'lastused', + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 + # Change bugs.delta_ts type from timestamp to datetime + my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); + if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('bugs', 'delta_ts', {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-09-24 - bugreport@peshkin.net, bug 307602 + # Make sure that default 4G table limit is overridden + my $attach_data_create = $self->selectrow_array( + 'SELECT CREATE_OPTIONS FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data' + ); + if ($attach_data_create !~ /MAX_ROWS/i) { + print "Converting attach_data maximum size to 100G...\n"; + $self->do( + "ALTER TABLE attach_data AVG_ROW_LENGTH=1000000, - MAX_ROWS=100000"); - } - - # Convert the database to UTF-8 if the utf8 parameter is on. - # We check if any table isn't utf8, because lots of crazy - # partial-conversion situations can happen, and this handles anything - # that could come up (including having the DB charset be utf8 but not - # the table charsets. - # - # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. - my $non_utf8_tables = $self->selectrow_array( - "SELECT 1 FROM information_schema.TABLES + MAX_ROWS=100000" + ); + } + + # Convert the database to UTF-8 if the utf8 parameter is on. + # We check if any table isn't utf8, because lots of crazy + # partial-conversion situations can happen, and this handles anything + # that could come up (including having the DB charset be utf8 but not + # the table charsets. + # + # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. + my $non_utf8_tables = $self->selectrow_array( + "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL AND TABLE_COLLATION NOT LIKE 'utf8%' - LIMIT 1", undef, $db_name); - - if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { - print "\n", install_string('mysql_utf8_conversion'); - - if (!Bugzilla->installation_answers->{NO_PAUSE}) { - if (Bugzilla->installation_mode == - INSTALLATION_MODE_NON_INTERACTIVE) - { - die install_string('continue_without_answers'), "\n"; - } - else { - print "\n " . install_string('enter_or_ctrl_c'); - getc; - } - } - - print "Converting table storage format to UTF-8. This may take a", - " while.\n"; - foreach my $table ($self->bz_table_list_real) { - my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table"); - $info_sth->execute(); - my (@binary_sql, @utf8_sql); - while (my $column = $info_sth->fetchrow_hashref) { - # Our conversion code doesn't work on enum fields, but they - # all go away later in checksetup anyway. - next if $column->{Type} =~ /enum/i; - - # If this particular column isn't stored in utf-8 - if ($column->{Collation} - && $column->{Collation} ne 'NULL' - && $column->{Collation} !~ /utf8/) - { - my $name = $column->{Field}; - - print "$table.$name needs to be converted to UTF-8...\n"; - - # These will be automatically re-created at the end - # of checksetup. - $self->bz_drop_related_fks($table, $name); - - my $col_info = - $self->bz_column_info_real($table, $name); - # CHANGE COLUMN doesn't take PRIMARY KEY - delete $col_info->{PRIMARYKEY}; - my $sql_def = $self->_bz_schema->get_type_ddl($col_info); - # We don't want MySQL to actually try to *convert* - # from our current charset to UTF-8, we just want to - # transfer the bytes directly. This is how we do that. - - # The CHARACTER SET part of the definition has to come - # right after the type, which will always come first. - my ($binary, $utf8) = ($sql_def, $sql_def); - my $type = $self->_bz_schema->convert_type($col_info->{TYPE}); - $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/; - $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/; - push(@binary_sql, "MODIFY COLUMN $name $binary"); - push(@utf8_sql, "MODIFY COLUMN $name $utf8"); - } - } # foreach column - - if (@binary_sql) { - my %indexes = %{ $self->bz_table_indexes($table) }; - foreach my $index_name (keys %indexes) { - my $index = $indexes{$index_name}; - if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') { - $self->bz_drop_index($table, $index_name); - } - else { - delete $indexes{$index_name}; - } - } - - print "Converting the $table table to UTF-8...\n"; - my $bin = "ALTER TABLE $table " . join(', ', @binary_sql); - my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql, - 'DEFAULT CHARACTER SET utf8'); - $self->do($bin); - $self->do($utf); - - # Re-add any removed FULLTEXT indexes. - foreach my $index (keys %indexes) { - $self->bz_add_index($table, $index, $indexes{$index}); - } - } - else { - $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8"); - } - - } # foreach my $table (@tables) + LIMIT 1", undef, $db_name + ); + + if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { + print "\n", install_string('mysql_utf8_conversion'); + + if (!Bugzilla->installation_answers->{NO_PAUSE}) { + if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) { + die install_string('continue_without_answers'), "\n"; + } + else { + print "\n " . install_string('enter_or_ctrl_c'); + getc; + } } - # Sometimes you can have a situation where all the tables are utf8, - # but the database isn't. (This tends to happen when you've done - # a mysqldump.) So we have this change outside of the above block, - # so that it just happens silently if no actual *table* conversion - # needs to happen. - if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { - $self->_alter_db_charset_to_utf8(); - } + print "Converting table storage format to UTF-8. This may take a", " while.\n"; + foreach my $table ($self->bz_table_list_real) { + my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table"); + $info_sth->execute(); + my (@binary_sql, @utf8_sql); + while (my $column = $info_sth->fetchrow_hashref) { + + # Our conversion code doesn't work on enum fields, but they + # all go away later in checksetup anyway. + next if $column->{Type} =~ /enum/i; + + # If this particular column isn't stored in utf-8 + if ( $column->{Collation} + && $column->{Collation} ne 'NULL' + && $column->{Collation} !~ /utf8/) + { + my $name = $column->{Field}; - $self->_fix_defaults(); + print "$table.$name needs to be converted to UTF-8...\n"; - # Bug 451735 highlighted a bug in bz_drop_index() which didn't - # check for FKs before trying to delete an index. Consequently, - # the series_creator_idx index was considered to be deleted - # despite it was still present in the DB. That's why we have to - # force the deletion, bypassing the DB schema. - if (!$self->bz_index_info('series', 'series_category_idx')) { - if (!$self->bz_index_info('series', 'series_creator_idx') - && $self->bz_index_info_real('series', 'series_creator_idx')) - { - foreach my $column (qw(creator category subcategory name)) { - $self->bz_drop_related_fks('series', $column); - } - $self->bz_drop_index_raw('series', 'series_creator_idx'); + # These will be automatically re-created at the end + # of checksetup. + $self->bz_drop_related_fks($table, $name); + + my $col_info = $self->bz_column_info_real($table, $name); + + # CHANGE COLUMN doesn't take PRIMARY KEY + delete $col_info->{PRIMARYKEY}; + my $sql_def = $self->_bz_schema->get_type_ddl($col_info); + + # We don't want MySQL to actually try to *convert* + # from our current charset to UTF-8, we just want to + # transfer the bytes directly. This is how we do that. + + # The CHARACTER SET part of the definition has to come + # right after the type, which will always come first. + my ($binary, $utf8) = ($sql_def, $sql_def); + my $type = $self->_bz_schema->convert_type($col_info->{TYPE}); + $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/; + $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/; + push(@binary_sql, "MODIFY COLUMN $name $binary"); + push(@utf8_sql, "MODIFY COLUMN $name $utf8"); } + } # foreach column + + if (@binary_sql) { + my %indexes = %{$self->bz_table_indexes($table)}; + foreach my $index_name (keys %indexes) { + my $index = $indexes{$index_name}; + if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') { + $self->bz_drop_index($table, $index_name); + } + else { + delete $indexes{$index_name}; + } + } + + print "Converting the $table table to UTF-8...\n"; + my $bin = "ALTER TABLE $table " . join(', ', @binary_sql); + my $utf + = "ALTER TABLE $table " . join(', ', @utf8_sql, 'DEFAULT CHARACTER SET utf8'); + $self->do($bin); + $self->do($utf); + + # Re-add any removed FULLTEXT indexes. + foreach my $index (keys %indexes) { + $self->bz_add_index($table, $index, $indexes{$index}); + } + } + else { + $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8"); + } + + } # foreach my $table (@tables) + } + + # Sometimes you can have a situation where all the tables are utf8, + # but the database isn't. (This tends to happen when you've done + # a mysqldump.) So we have this change outside of the above block, + # so that it just happens silently if no actual *table* conversion + # needs to happen. + if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { + $self->_alter_db_charset_to_utf8(); + } + + $self->_fix_defaults(); + + # Bug 451735 highlighted a bug in bz_drop_index() which didn't + # check for FKs before trying to delete an index. Consequently, + # the series_creator_idx index was considered to be deleted + # despite it was still present in the DB. That's why we have to + # force the deletion, bypassing the DB schema. + if (!$self->bz_index_info('series', 'series_category_idx')) { + if (!$self->bz_index_info('series', 'series_creator_idx') + && $self->bz_index_info_real('series', 'series_creator_idx')) + { + foreach my $column (qw(creator category subcategory name)) { + $self->bz_drop_related_fks('series', $column); + } + $self->bz_drop_index_raw('series', 'series_creator_idx'); } + } } # When you import a MySQL 3/4 mysqldump into MySQL 5, columns that @@ -792,100 +809,109 @@ sub bz_setup_database { # looks like. So we remove defaults from columns that aren't supposed # to have them sub _fix_defaults { - my $self = shift; - my $maj_version = substr($self->bz_server_version, 0, 1); - return if $maj_version < 5; - - # The oldest column that could have this problem is bugs.assigned_to, - # so if it doesn't have the problem, we just skip doing this entirely. - my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); - my $assi_default = $assi_def->{COLUMN_DEF}; - # This "ne ''" thing is necessary because _raw_column_info seems to - # return COLUMN_DEF as an empty string for columns that don't have - # a default. - return unless (defined $assi_default && $assi_default ne ''); - - my %fix_columns; - foreach my $table ($self->_bz_real_schema->get_table_list()) { - foreach my $column ($self->bz_table_columns($table)) { - my $abs_def = $self->bz_column_info($table, $column); - # BLOB/TEXT columns never have defaults - next if $abs_def->{TYPE} =~ /BLOB|TEXT/i; - if (!defined $abs_def->{DEFAULT}) { - # Get the exact default from the database without any - # "fixing" by bz_column_info_real. - my $raw_info = $self->_bz_raw_column_info($table, $column); - my $raw_default = $raw_info->{COLUMN_DEF}; - if (defined $raw_default) { - if ($raw_default eq '') { - # Only (var)char columns can have empty strings as - # defaults, so if we got an empty string for some - # other default type, then it's bogus. - next unless $abs_def->{TYPE} =~ /char/i; - $raw_default = "''"; - } - $fix_columns{$table} ||= []; - push(@{ $fix_columns{$table} }, $column); - print "$table.$column has incorrect DB default: $raw_default\n"; - } - } - } # foreach $column - } # foreach $table - - print "Fixing defaults...\n"; - foreach my $table (reverse sort keys %fix_columns) { - my @alters = map("ALTER COLUMN $_ DROP DEFAULT", - @{ $fix_columns{$table} }); - my $sql = "ALTER TABLE $table " . join(',', @alters); - $self->do($sql); - } + my $self = shift; + my $maj_version = substr($self->bz_server_version, 0, 1); + return if $maj_version < 5; + + # The oldest column that could have this problem is bugs.assigned_to, + # so if it doesn't have the problem, we just skip doing this entirely. + my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); + my $assi_default = $assi_def->{COLUMN_DEF}; + + # This "ne ''" thing is necessary because _raw_column_info seems to + # return COLUMN_DEF as an empty string for columns that don't have + # a default. + return unless (defined $assi_default && $assi_default ne ''); + + my %fix_columns; + foreach my $table ($self->_bz_real_schema->get_table_list()) { + foreach my $column ($self->bz_table_columns($table)) { + my $abs_def = $self->bz_column_info($table, $column); + + # BLOB/TEXT columns never have defaults + next if $abs_def->{TYPE} =~ /BLOB|TEXT/i; + if (!defined $abs_def->{DEFAULT}) { + + # Get the exact default from the database without any + # "fixing" by bz_column_info_real. + my $raw_info = $self->_bz_raw_column_info($table, $column); + my $raw_default = $raw_info->{COLUMN_DEF}; + if (defined $raw_default) { + if ($raw_default eq '') { + + # Only (var)char columns can have empty strings as + # defaults, so if we got an empty string for some + # other default type, then it's bogus. + next unless $abs_def->{TYPE} =~ /char/i; + $raw_default = "''"; + } + $fix_columns{$table} ||= []; + push(@{$fix_columns{$table}}, $column); + print "$table.$column has incorrect DB default: $raw_default\n"; + } + } + } # foreach $column + } # foreach $table + + print "Fixing defaults...\n"; + foreach my $table (reverse sort keys %fix_columns) { + my @alters = map("ALTER COLUMN $_ DROP DEFAULT", @{$fix_columns{$table}}); + my $sql = "ALTER TABLE $table " . join(',', @alters); + $self->do($sql); + } } sub _alter_db_charset_to_utf8 { - my $self = shift; - my $db_name = Bugzilla->localconfig->{db_name}; - $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); + my $self = shift; + my $db_name = Bugzilla->localconfig->{db_name}; + $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); } sub bz_db_is_utf8 { - my $self = shift; - my $db_collation = $self->selectrow_arrayref( - "SHOW VARIABLES LIKE 'character_set_database'"); - # First column holds the variable name, second column holds the value. - return $db_collation->[1] =~ /utf8/ ? 1 : 0; + my $self = shift; + my $db_collation + = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'"); + + # First column holds the variable name, second column holds the value. + return $db_collation->[1] =~ /utf8/ ? 1 : 0; } sub bz_enum_initial_values { - my ($self) = @_; - my %enum_values = %{$self->ENUM_DEFAULTS}; - # Get a complete description of the 'bugs' table; with DBD::MySQL - # there isn't a column-by-column way of doing this. Could use - # $dbh->column_info, but it would go slower and we would have to - # use the undocumented mysql_type_name accessor to get the type - # of each row. - my $sth = $self->prepare("DESCRIBE bugs"); - $sth->execute(); - # Look for the particular columns we are interested in. - while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { - if (defined $enum_values{$thiscol}) { - # this is a column of interest. - my @value_list; - if ($thistype and ($thistype =~ /^enum\(/)) { - # it has an enum type; get the set of values. - while ($thistype =~ /'([^']*)'(.*)/) { - push(@value_list, $1); - $thistype = $2; - } - } - if (@value_list) { - # record the enum values found. - $enum_values{$thiscol} = \@value_list; - } + my ($self) = @_; + my %enum_values = %{$self->ENUM_DEFAULTS}; + + # Get a complete description of the 'bugs' table; with DBD::MySQL + # there isn't a column-by-column way of doing this. Could use + # $dbh->column_info, but it would go slower and we would have to + # use the undocumented mysql_type_name accessor to get the type + # of each row. + my $sth = $self->prepare("DESCRIBE bugs"); + $sth->execute(); + + # Look for the particular columns we are interested in. + while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { + if (defined $enum_values{$thiscol}) { + + # this is a column of interest. + my @value_list; + if ($thistype and ($thistype =~ /^enum\(/)) { + + # it has an enum type; get the set of values. + while ($thistype =~ /'([^']*)'(.*)/) { + push(@value_list, $1); + $thistype = $2; } + } + if (@value_list) { + + # record the enum values found. + $enum_values{$thiscol} = \@value_list; + } } + } - return \%enum_values; + return \%enum_values; } ##################################################################### @@ -916,29 +942,29 @@ backwards-compatibility anyway, for versions of Bugzilla before 2.20. =cut sub bz_column_info_real { - my ($self, $table, $column) = @_; - my $col_data = $self->_bz_raw_column_info($table, $column); - return $self->_bz_schema->column_info_to_column($col_data); + my ($self, $table, $column) = @_; + my $col_data = $self->_bz_raw_column_info($table, $column); + return $self->_bz_schema->column_info_to_column($col_data); } sub _bz_raw_column_info { - my ($self, $table, $column) = @_; - - # DBD::mysql does not support selecting a specific column, - # so we have to get all the columns on the table and find - # the one we want. - my $info_sth = $self->column_info(undef, undef, $table, '%'); - - # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) - my $col_data; - while ($col_data = $info_sth->fetchrow_hashref) { - last if $col_data->{'COLUMN_NAME'} eq $column; - } - - if (!defined $col_data) { - return undef; - } - return $col_data; + my ($self, $table, $column) = @_; + + # DBD::mysql does not support selecting a specific column, + # so we have to get all the columns on the table and find + # the one we want. + my $info_sth = $self->column_info(undef, undef, $table, '%'); + + # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) + my $col_data; + while ($col_data = $info_sth->fetchrow_hashref) { + last if $col_data->{'COLUMN_NAME'} eq $column; + } + + if (!defined $col_data) { + return undef; + } + return $col_data; } =item C<bz_index_info_real($table, $index)> @@ -952,42 +978,43 @@ sub _bz_raw_column_info { =cut sub bz_index_info_real { - my ($self, $table, $index) = @_; - - my $sth = $self->prepare("SHOW INDEX FROM $table"); - $sth->execute; - - my @fields; - my $index_type; - # $raw_def will be an arrayref containing the following information: - # 0 = name of the table that the index is on - # 1 = 0 if unique, 1 if not unique - # 2 = name of the index - # 3 = seq_in_index (The order of the current field in the index). - # 4 = Name of ONE column that the index is on - # 5 = 'Collation' of the index. Usually 'A'. - # 6 = Cardinality. Either a number or undef. - # 7 = sub_part. Usually undef. Sometimes 1. - # 8 = "packed". Usually undef. - # 9 = Null. Sometimes undef, sometimes 'YES'. - # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' - # 11 = 'Comment.' Usually undef. - while (my $raw_def = $sth->fetchrow_arrayref) { - if ($raw_def->[2] eq $index) { - push(@fields, $raw_def->[4]); - # No index can be both UNIQUE and FULLTEXT, that's why - # this is written this way. - $index_type = $raw_def->[1] ? '' : 'UNIQUE'; - $index_type = $raw_def->[10] eq 'FULLTEXT' - ? 'FULLTEXT' : $index_type; - } + my ($self, $table, $index) = @_; + + my $sth = $self->prepare("SHOW INDEX FROM $table"); + $sth->execute; + + my @fields; + my $index_type; + + # $raw_def will be an arrayref containing the following information: + # 0 = name of the table that the index is on + # 1 = 0 if unique, 1 if not unique + # 2 = name of the index + # 3 = seq_in_index (The order of the current field in the index). + # 4 = Name of ONE column that the index is on + # 5 = 'Collation' of the index. Usually 'A'. + # 6 = Cardinality. Either a number or undef. + # 7 = sub_part. Usually undef. Sometimes 1. + # 8 = "packed". Usually undef. + # 9 = Null. Sometimes undef, sometimes 'YES'. + # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' + # 11 = 'Comment.' Usually undef. + while (my $raw_def = $sth->fetchrow_arrayref) { + if ($raw_def->[2] eq $index) { + push(@fields, $raw_def->[4]); + + # No index can be both UNIQUE and FULLTEXT, that's why + # this is written this way. + $index_type = $raw_def->[1] ? '' : 'UNIQUE'; + $index_type = $raw_def->[10] eq 'FULLTEXT' ? 'FULLTEXT' : $index_type; } + } - my $retval; - if (scalar(@fields)) { - $retval = {FIELDS => \@fields, TYPE => $index_type}; - } - return $retval; + my $retval; + if (scalar(@fields)) { + $retval = {FIELDS => \@fields, TYPE => $index_type}; + } + return $retval; } =item C<bz_index_list_real($table)> @@ -1000,10 +1027,11 @@ sub bz_index_info_real { =cut sub bz_index_list_real { - my ($self, $table) = @_; - my $sth = $self->prepare("SHOW INDEX FROM $table"); - # Column 3 of a SHOW INDEX statement contains the name of the index. - return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) }; + my ($self, $table) = @_; + my $sth = $self->prepare("SHOW INDEX FROM $table"); + + # Column 3 of a SHOW INDEX statement contains the name of the index. + return @{$self->selectcol_arrayref($sth, {Columns => [3]})}; } ##################################################################### @@ -1027,34 +1055,33 @@ this code does. # bz_column_info_real function would be very difficult to create # properly for any other DB besides MySQL. sub _bz_build_schema_from_disk { - my ($self) = @_; - - my $schema = $self->_bz_schema->get_empty_schema(); - - my @tables = $self->bz_table_list_real(); - if (@tables) { - print "Building Schema object from database...\n"; + my ($self) = @_; + + my $schema = $self->_bz_schema->get_empty_schema(); + + my @tables = $self->bz_table_list_real(); + if (@tables) { + print "Building Schema object from database...\n"; + } + foreach my $table (@tables) { + $schema->add_table($table); + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $type_info = $self->bz_column_info_real($table, $column); + $schema->set_column($table, $column, $type_info); } - foreach my $table (@tables) { - $schema->add_table($table); - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $type_info = $self->bz_column_info_real($table, $column); - $schema->set_column($table, $column, $type_info); - } - my @indexes = $self->bz_index_list_real($table); - foreach my $index (@indexes) { - unless ($index eq 'PRIMARY') { - my $index_info = $self->bz_index_info_real($table, $index); - ($index_info = $index_info->{FIELDS}) - if (!$index_info->{TYPE}); - $schema->set_index($table, $index, $index_info); - } - } + my @indexes = $self->bz_index_list_real($table); + foreach my $index (@indexes) { + unless ($index eq 'PRIMARY') { + my $index_info = $self->bz_index_info_real($table, $index); + ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE}); + $schema->set_index($table, $index, $index_info); + } } + } - return $schema; + return $schema; } 1; diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm index 7424019ac..930270ccc 100644 --- a/Bugzilla/DB/Oracle.pm +++ b/Bugzilla/DB/Oracle.pm @@ -38,461 +38,473 @@ use Bugzilla::Util; ##################################################################### # Constants ##################################################################### -use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; +use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; use constant ISOLATION_LEVEL => 'READ COMMITTED'; -use constant BLOB_TYPE => { ora_type => ORA_BLOB }; +use constant BLOB_TYPE => {ora_type => ORA_BLOB}; + # The max size allowed for LOB fields, in kilobytes. use constant MIN_LONG_READ_LEN => 32 * 1024; -use constant FULLTEXT_OR => ' OR '; +use constant FULLTEXT_OR => ' OR '; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; - - # You can never connect to Oracle without a DB name, - # and there is no default DB. - $dbname ||= Bugzilla->localconfig->{db_name}; - - # Set the language enviroment - $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; - $dsn .= ";port=$port" if $port; - my $attrs = { FetchHashKeyName => 'NAME_lc', - LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0, - MIN_LONG_READ_LEN) * 1024, - }; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - bless ($self, $class); - - # Set the session's default date format to match MySQL - $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") - if Bugzilla->params->{'utf8'}; - # To allow case insensitive query. - $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); - $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); - return $self; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; + + # You can never connect to Oracle without a DB name, + # and there is no default DB. + $dbname ||= Bugzilla->localconfig->{db_name}; + + # Set the language enviroment + $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; + $dsn .= ";port=$port" if $port; + my $attrs = { + FetchHashKeyName => 'NAME_lc', + LongReadLen => + max(Bugzilla->params->{'maxattachmentsize'} || 0, MIN_LONG_READ_LEN) * 1024, + }; + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Set the session's default date format to match MySQL + $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") + if Bugzilla->params->{'utf8'}; + + # To allow case insensitive query. + $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); + $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); + return $self; } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_SEQ"; - my ($last_insert_id) = $self->selectrow_array("SELECT $seq.CURRVAL " - . " FROM DUAL"); - return $last_insert_id; + my $seq = $table . "_" . $column . "_SEQ"; + my ($last_insert_id) + = $self->selectrow_array("SELECT $seq.CURRVAL " . " FROM DUAL"); + return $last_insert_id; } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT 1 FROM DUAL WHERE " - . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT 1 FROM DUAL WHERE " + . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } -sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); - $sth->execute(); - my $explain = $self->selectcol_arrayref( - "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); - return join("\n", @$explain); -} +sub bz_explain { + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); + $sth->execute(); + my $explain = $self->selectcol_arrayref( + "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); + return join("\n", @$explain); +} sub sql_group_concat { - my ($self, $text, $separator) = @_; - $separator = $self->quote(', ') if !defined $separator; - my ($distinct, $rest) = $text =~/^(\s*DISTINCT\s|)(.+)$/i; - return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; + my ($self, $text, $separator) = @_; + $separator = $self->quote(', ') if !defined $separator; + my ($distinct, $rest) = $text =~ /^(\s*DISTINCT\s|)(.+)$/i; + return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "REGEXP_LIKE($expr, $pattern)"; + return "REGEXP_LIKE($expr, $pattern)"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "NOT REGEXP_LIKE($expr, $pattern)" + return "NOT REGEXP_LIKE($expr, $pattern)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; + my ($self, $limit, $offset) = @_; - if(defined $offset) { - return "/* LIMIT $limit $offset */"; - } - return "/* LIMIT $limit */"; + if (defined $offset) { + return "/* LIMIT $limit $offset */"; + } + return "/* LIMIT $limit */"; } sub sql_string_concat { - my ($self, @params) = @_; + my ($self, @params) = @_; - return 'CONCAT(' . join(', ', @params) . ')'; + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return " TO_CHAR(TO_DATE($date),'J') "; + return " TO_CHAR(TO_DATE($date),'J') "; } -sub sql_from_days{ - my ($self, $date) = @_; - return " TO_DATE($date,'J') "; +sub sql_from_days { + my ($self, $date) = @_; + + return " TO_DATE($date,'J') "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - state $label = 0; - $text = $self->quote($text); - trick_taint($text); - $label++; - return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; + my ($self, $column, $text) = @_; + state $label = 0; + $text = $self->quote($text); + trick_taint($text); + $label++; + return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; - return "TO_CHAR($date, " . $self->quote($format) . ")"; + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - my $time_sql; - if ($units =~ /YEAR|MONTH/i) { - $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; - } else{ - $time_sql = "NUMTODSINTERVAL($interval,'$units')"; - } - return "$date $operator $time_sql"; + my ($self, $date, $operator, $interval, $units) = @_; + my $time_sql; + if ($units =~ /YEAR|MONTH/i) { + $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; + } + else { + $time_sql = "NUMTODSINTERVAL($interval,'$units')"; + } + return "$date $operator $time_sql"; } sub sql_position { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - my @in_list = @$in_list_ref; - return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) if $#in_list < 1000; - my @in_str; - while (@in_list) { - my $length = $#in_list + 1; - my $splice = $length > 1000 ? 1000 : $length; - my @sub_in_list = splice(@in_list, 0, $splice); - push(@in_str, - $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); - } - return "( " . join(" OR ", @in_str) . " )"; + my ($self, $column_name, $in_list_ref, $negate) = @_; + my @in_list = @$in_list_ref; + return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) + if $#in_list < 1000; + my @in_str; + while (@in_list) { + my $length = $#in_list + 1; + my $splice = $length > 1000 ? 1000 : $length; + my @sub_in_list = splice(@in_list, 0, $splice); + push(@in_str, $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); + } + return "( " . join(" OR ", @in_str) . " )"; } sub _bz_add_field_table { - my ($self, $name, $schema_ref, $type) = @_; - $self->SUPER::_bz_add_field_table($name, $schema_ref); - if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { - my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); - $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); - } + my ($self, $name, $schema_ref, $type) = @_; + $self->SUPER::_bz_add_field_table($name, $schema_ref); + if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { + my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); + $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); + } } sub bz_drop_table { - my ($self, $name) = @_; - my $table_exists = $self->bz_table_info($name); - if ($table_exists) { - $self->_bz_drop_fks($name); - $self->SUPER::bz_drop_table($name); - } + my ($self, $name) = @_; + my $table_exists = $self->bz_table_info($name); + if ($table_exists) { + $self->_bz_drop_fks($name); + $self->SUPER::bz_drop_table($name); + } } -# Dropping all FKs for a specified table. +# Dropping all FKs for a specified table. sub _bz_drop_fks { - my ($self, $table) = @_; - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my ($self, $table) = @_; + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); + } } sub _fix_empty { - my ($string) = @_; - $string = '' if $string eq EMPTY_STRING; - return $string; + my ($string) = @_; + $string = '' if $string eq EMPTY_STRING; + return $string; } sub _fix_arrayref { - my ($row) = @_; - return undef if !defined $row; - foreach my $field (@$row) { - $field = _fix_empty($field) if defined $field; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $field (@$row) { + $field = _fix_empty($field) if defined $field; + } + return $row; } sub _fix_hashref { - my ($row) = @_; - return undef if !defined $row; - foreach my $value (values %$row) { - $value = _fix_empty($value) if defined $value; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $value (values %$row) { + $value = _fix_empty($value) if defined $value; + } + return $row; } sub adjust_statement { - my ($sql) = @_; - - if ($sql =~ /^CREATE OR REPLACE.*/i){ - return $sql; - } - - # We can't just assume any occurrence of "''" in $sql is an empty - # string, since "''" can occur inside a string literal as a way of - # escaping a single "'" in the literal. Therefore we must be trickier... - - # split the statement into parts by single-quotes. The negative value - # at the end to the split operator from dropping trailing empty strings - # (e.g., when $sql ends in "''") - my @parts = split /'/, $sql, -1; - - if( !(@parts % 2) ) { - # Either the string is empty or the quotes are mismatched - # Returning input unmodified. - return $sql; + my ($sql) = @_; + + if ($sql =~ /^CREATE OR REPLACE.*/i) { + return $sql; + } + + # We can't just assume any occurrence of "''" in $sql is an empty + # string, since "''" can occur inside a string literal as a way of + # escaping a single "'" in the literal. Therefore we must be trickier... + + # split the statement into parts by single-quotes. The negative value + # at the end to the split operator from dropping trailing empty strings + # (e.g., when $sql ends in "''") + my @parts = split /'/, $sql, -1; + + if (!(@parts % 2)) { + + # Either the string is empty or the quotes are mismatched + # Returning input unmodified. + return $sql; + } + + # We already verified that we have an odd number of parts. If we take + # the first part off now, we know we're entering the loop with an even + # number of parts + my @result; + my $part = shift @parts; + + # Oracle requires a FROM clause in all SELECT statements, so append + # "FROM dual" to queries without one (e.g., "SELECT NOW()") + my $is_select = ($part =~ m/^\s*SELECT\b/io); + my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + + # Oracle includes the time in CURRENT_DATE. + $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + + # Oracle use SUBSTR instead of SUBSTRING + $part =~ s/\bSUBSTRING\b/SUBSTR/io; + + # Oracle need no 'AS' + $part =~ s/\bAS\b//ig; + + # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the + # query with "SELECT * FROM (...) WHERE rownum < $limit" + my ($limit, $offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); + + push @result, $part; + while (@parts) { + my $string = shift @parts; + my $nonstring = shift @parts; + + # if the non-string part is zero-length and there are more parts left, + # then this is an escaped quote inside a string literal + while (!(length $nonstring) && @parts) { + + # we know it's safe to remove two parts at a time, since we + # entered the loop with an even number of parts + $string .= "''" . shift @parts; + $nonstring = shift @parts; } - # We already verified that we have an odd number of parts. If we take - # the first part off now, we know we're entering the loop with an even - # number of parts - my @result; - my $part = shift @parts; - - # Oracle requires a FROM clause in all SELECT statements, so append - # "FROM dual" to queries without one (e.g., "SELECT NOW()") - my $is_select = ($part =~ m/^\s*SELECT\b/io); - my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + # Look for a FROM if this is a SELECT and we haven't found one yet + $has_from = ($nonstring =~ m/\bFROM\b/io) if ($is_select and !$has_from); # Oracle includes the time in CURRENT_DATE. - $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; # Oracle use SUBSTR instead of SUBSTRING - $part =~ s/\bSUBSTRING\b/SUBSTR/io; - + $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + # Oracle need no 'AS' - $part =~ s/\bAS\b//ig; - - # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the - # query with "SELECT * FROM (...) WHERE rownum < $limit" - my ($limit,$offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); - - push @result, $part; - while( @parts ) { - my $string = shift @parts; - my $nonstring = shift @parts; - - # if the non-string part is zero-length and there are more parts left, - # then this is an escaped quote inside a string literal - while( !(length $nonstring) && @parts ) { - # we know it's safe to remove two parts at a time, since we - # entered the loop with an even number of parts - $string .= "''" . shift @parts; - $nonstring = shift @parts; - } + $nonstring =~ s/\bAS\b//ig; - # Look for a FROM if this is a SELECT and we haven't found one yet - $has_from = ($nonstring =~ m/\bFROM\b/io) - if ($is_select and !$has_from); + # Look for a LIMIT clause + ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); - # Oracle includes the time in CURRENT_DATE. - $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + if (!length($string)) { + push @result, EMPTY_STRING; + push @result, $nonstring; + } + else { + push @result, $string; + push @result, $nonstring; + } + } - # Oracle use SUBSTR instead of SUBSTRING - $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + my $new_sql = join "'", @result; - # Oracle need no 'AS' - $nonstring =~ s/\bAS\b//ig; + # Append "FROM dual" if this is a SELECT without a FROM clause + $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - # Look for a LIMIT clause - ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); + # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - if(!length($string)){ - push @result, EMPTY_STRING; - push @result, $nonstring; - } else { - push @result, $string; - push @result, $nonstring; - } + if (defined($limit)) { + if ($new_sql !~ /\bWHERE\b/) { + $new_sql = $new_sql . " WHERE 1=1"; } - - my $new_sql = join "'", @result; - - # Append "FROM dual" if this is a SELECT without a FROM clause - $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - - # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - - if (defined($limit)) { - if ($new_sql !~ /\bWHERE\b/) { - $new_sql = $new_sql." WHERE 1=1"; - } - my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); - if (defined($offset)) { - my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); - $before_where = "$before_from FROM ($before_from," - . " ROW_NUMBER() OVER (ORDER BY 1) R " - . " FROM $after_from ) "; - $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; - } else { - $after_where = " rownum <=$limit AND ".$after_where; - } - $new_sql = $before_where." WHERE ".$after_where; + my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); + if (defined($offset)) { + my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); + $before_where + = "$before_from FROM ($before_from," + . " ROW_NUMBER() OVER (ORDER BY 1) R " + . " FROM $after_from ) "; + $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; + } + else { + $after_where = " rownum <=$limit AND " . $after_where; } - return $new_sql; + $new_sql = $before_where . " WHERE " . $after_where; + } + return $new_sql; } sub do { - my $self = shift; - my $sql = shift; - $sql = adjust_statement($sql); - unshift @_, $sql; - return $self->SUPER::do(@_); + my $self = shift; + my $sql = shift; + $sql = adjust_statement($sql); + unshift @_, $sql; + return $self->SUPER::do(@_); } sub selectrow_array { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - if ( wantarray ) { - my @row = $self->SUPER::selectrow_array(@_); - _fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::selectrow_array(@_); - $row = _fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + if (wantarray) { + my @row = $self->SUPER::selectrow_array(@_); + _fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::selectrow_array(@_); + $row = _fix_empty($row) if defined $row; + return $row; + } } sub selectrow_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_arrayref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_arrayref(@_); + return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + _fix_arrayref($ref); + return $ref; } sub selectrow_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_hashref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_hashref(@_); + return undef if !defined $ref; - _fix_hashref($ref); - return $ref; + _fix_hashref($ref); + return $ref; } sub selectall_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectall_arrayref(@_); - return undef if !defined $ref; - - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - _fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - _fix_hashref($row); - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectall_arrayref(@_); + return undef if !defined $ref; + + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + _fix_arrayref($row); } + elsif (ref($row) eq 'HASH') { + _fix_hashref($row); + } + } - return $ref; + return $ref; } sub selectall_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $rows = $self->SUPER::selectall_hashref(@_); - return undef if !defined $rows; - foreach my $row (values %$rows) { - _fix_hashref($row); - } - return $rows; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $rows = $self->SUPER::selectall_hashref(@_); + return undef if !defined $rows; + foreach my $row (values %$rows) { + _fix_hashref($row); + } + return $rows; } sub selectcol_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectcol_arrayref(@_); - return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectcol_arrayref(@_); + return undef if !defined $ref; + _fix_arrayref($ref); + return $ref; } sub prepare { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare(@_), 'Bugzilla::DB::Oracle::st'; } sub prepare_cached { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare_cached(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare_cached(@_), 'Bugzilla::DB::Oracle::st'; } sub quote_identifier { - my ($self,$id) = @_; - return $id; + my ($self, $id) = @_; + return $id; } ##################################################################### @@ -500,20 +512,22 @@ sub quote_identifier { ##################################################################### sub bz_table_columns_real { - my ($self, $table) = @_; - $table = uc($table); - my $cols = $self->selectcol_arrayref( - "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE - TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table); - return @$cols; + my ($self, $table) = @_; + $table = uc($table); + my $cols = $self->selectcol_arrayref( + "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE + TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table + ); + return @$cols; } sub bz_table_list_real { - my ($self) = @_; - my $tables = $self->selectcol_arrayref( - "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE - TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%'); - return @$tables; + my ($self) = @_; + my $tables = $self->selectcol_arrayref( + "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE + TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%' + ); + return @$tables; } ##################################################################### @@ -521,32 +535,37 @@ sub bz_table_list_real { ##################################################################### sub bz_setup_database { - my $self = shift; - - # Create a function that returns SYSDATE to emulate MySQL's "NOW()". - # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not - # have that function, So we have to create one ourself. - $self->do("CREATE OR REPLACE FUNCTION NOW " - . " RETURN DATE IS BEGIN RETURN SYSDATE; END;"); - $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" - . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;"); - - # Create types for group_concat - my $type_exists = $self->selectrow_array("SELECT 1 FROM user_types - WHERE type_name = 'T_GROUP_CONCAT'"); - $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; - $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " - . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" - . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" - . ");"); - $self->do("CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS + my $self = shift; + + # Create a function that returns SYSDATE to emulate MySQL's "NOW()". + # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not + # have that function, So we have to create one ourself. + $self->do("CREATE OR REPLACE FUNCTION NOW " + . " RETURN DATE IS BEGIN RETURN SYSDATE; END;"); + $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" + . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;"); + + # Create types for group_concat + my $type_exists = $self->selectrow_array( + "SELECT 1 FROM user_types + WHERE type_name = 'T_GROUP_CONCAT'" + ); + $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; + $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " + . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" + . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" + . ");"); + $self->do( + "CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2 is BEGIN RETURN p_CONTENT; END; - END;"); + END;" + ); - $self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT + $self->do( + "CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT ( CLOB_CONTENT CLOB, DELIMITER VARCHAR2(256), STATIC FUNCTION ODCIAGGREGATEINITIALIZE( @@ -564,9 +583,11 @@ sub bz_setup_database { MEMBER FUNCTION ODCIAGGREGATEMERGE( SELF IN OUT NOCOPY T_GROUP_CONCAT, CTX2 IN T_GROUP_CONCAT) - RETURN NUMBER);"); + RETURN NUMBER);" + ); - $self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS + $self->do( + "CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS STATIC FUNCTION ODCIAGGREGATEINITIALIZE( SCTX IN OUT NOCOPY T_GROUP_CONCAT) RETURN NUMBER IS @@ -610,110 +631,117 @@ sub bz_setup_database { DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT); RETURN ODCICONST.SUCCESS; END; - END;"); + END;" + ); - # Create user-defined aggregate function group_concat - $self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) + # Create user-defined aggregate function group_concat + $self->do( + "CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) RETURN CLOB - DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;"); - - # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search - my $lexer = $self->selectcol_arrayref( - "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND - pre_owner = ?", - undef,'BZ_LEX',uc(Bugzilla->localconfig->{db_user})); - if(!@$lexer) { - $self->do("BEGIN CTX_DDL.CREATE_PREFERENCE - ('BZ_LEX', 'WORLD_LEXER'); END;"); - } - - $self->SUPER::bz_setup_database(@_); - - my $sth = $self->prepare("SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); - my @tables = $self->bz_table_list_real(); - - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $def = $self->bz_column_info($table, $column); - # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys - # correctly (bug 731156). We have to add missing sequences and - # triggers ourselves. - if ($def->{TYPE} =~ /SERIAL/i) { - my $sequence = "${table}_${column}_SEQ"; - my $exists = $self->selectrow_array($sth, undef, $sequence); - if (!$exists) { - my @sql = $self->_get_create_seq_ddl($table, $column); - $self->do($_) foreach @sql; - } - } - - if ($def->{REFERENCES}) { - my $references = $def->{REFERENCES}; - my $update = $references->{UPDATE} || 'CASCADE'; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = $self->_bz_schema->_get_fk_name($table, - $column, - $references); - # bz_rename_table didn't rename the trigger correctly. - if ($table eq 'bug_tag' && $to_table eq 'tags') { - $to_table = 'tag'; - } - if ( $update =~ /CASCADE/i ){ - my $trigger_name = uc($fk_name . "_UC"); - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } - - my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name" - . " AFTER UPDATE OF $to_column ON $to_table " - . " REFERENCING " - . " NEW AS NEW " - . " OLD AS OLD " - . " FOR EACH ROW " - . " BEGIN " - . " UPDATE $table" - . " SET $column = :NEW.$to_column" - . " WHERE $column = :OLD.$to_column;" - . " END $trigger_name;"; - $self->do($tr_str); - } - } + DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;" + ); + + # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search + my $lexer = $self->selectcol_arrayref( + "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND + pre_owner = ?", undef, 'BZ_LEX', uc(Bugzilla->localconfig->{db_user}) + ); + if (!@$lexer) { + $self->do( + "BEGIN CTX_DDL.CREATE_PREFERENCE + ('BZ_LEX', 'WORLD_LEXER'); END;" + ); + } + + $self->SUPER::bz_setup_database(@_); + + my $sth = $self->prepare( + "SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); + my @tables = $self->bz_table_list_real(); + + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $def = $self->bz_column_info($table, $column); + + # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys + # correctly (bug 731156). We have to add missing sequences and + # triggers ourselves. + if ($def->{TYPE} =~ /SERIAL/i) { + my $sequence = "${table}_${column}_SEQ"; + my $exists = $self->selectrow_array($sth, undef, $sequence); + if (!$exists) { + my @sql = $self->_get_create_seq_ddl($table, $column); + $self->do($_) foreach @sql; } + } + + if ($def->{REFERENCES}) { + my $references = $def->{REFERENCES}; + my $update = $references->{UPDATE} || 'CASCADE'; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = $self->_bz_schema->_get_fk_name($table, $column, $references); + + # bz_rename_table didn't rename the trigger correctly. + if ($table eq 'bug_tag' && $to_table eq 'tags') { + $to_table = 'tag'; + } + if ($update =~ /CASCADE/i) { + my $trigger_name = uc($fk_name . "_UC"); + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } + + my $tr_str + = "CREATE OR REPLACE TRIGGER $trigger_name" + . " AFTER UPDATE OF $to_column ON $to_table " + . " REFERENCING " + . " NEW AS NEW " + . " OLD AS OLD " + . " FOR EACH ROW " + . " BEGIN " + . " UPDATE $table" + . " SET $column = :NEW.$to_column" + . " WHERE $column = :OLD.$to_column;" + . " END $trigger_name;"; + $self->do($tr_str); + } + } } + } - # Drop the trigger which causes bug 541553 - my $trigger_name = "PRODUCTS_MILESTONEURL"; - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } + # Drop the trigger which causes bug 541553 + my $trigger_name = "PRODUCTS_MILESTONEURL"; + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } } # These two methods have been copied from Bugzilla::DB::Schema::Oracle. sub _get_create_seq_ddl { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " . - "NOMAXVALUE NOCYCLE NOCACHE"; - my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); - return ($seq_sql, $trigger_sql); + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " + . "NOMAXVALUE NOCYCLE NOCACHE"; + my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); + return ($seq_sql, $trigger_sql); } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; + my ($self, $table, $column, $seq_name) = @_; - my $trigger_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $trigger_sql; + my $trigger_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $trigger_sql; } ############################################################################ @@ -725,68 +753,69 @@ use strict; use warnings; use parent -norequire, qw(DBI::st); - + sub fetchrow_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_arrayref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_arrayref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_arrayref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_arrayref($ref); + return $ref; } sub fetchrow_array { - my $self = shift; - if ( wantarray ) { - my @row = $self->SUPER::fetchrow_array(@_); - Bugzilla::DB::Oracle::_fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::fetchrow_array(@_); - $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + if (wantarray) { + my @row = $self->SUPER::fetchrow_array(@_); + Bugzilla::DB::Oracle::_fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::fetchrow_array(@_); + $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; + return $row; + } } sub fetchrow_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_hashref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_hashref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_hashref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_hashref($ref); + return $ref; } sub fetchall_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchall_arrayref(@_); - return undef if !defined $ref; - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - Bugzilla::DB::Oracle::_fix_hashref($row); - } + my $self = shift; + my $ref = $self->SUPER::fetchall_arrayref(@_); + return undef if !defined $ref; + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + Bugzilla::DB::Oracle::_fix_arrayref($row); } - return $ref; + elsif (ref($row) eq 'HASH') { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + } + return $ref; } sub fetchall_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchall_hashref(@_); - return undef if !defined $ref; - foreach my $row (values %$ref) { - Bugzilla::DB::Oracle::_fix_hashref($row); - } - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchall_hashref(@_); + return undef if !defined $ref; + foreach my $row (values %$ref) { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + return $ref; } sub fetch { - my $self = shift; - my $row = $self->SUPER::fetch(@_); - if ($row) { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - return $row; + my $self = shift; + my $row = $self->SUPER::fetch(@_); + if ($row) { + Bugzilla::DB::Oracle::_fix_arrayref($row); + } + return $row; } 1; diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm index cbf8d7af1..15a268381 100644 --- a/Bugzilla/DB/Pg.pm +++ b/Bugzilla/DB/Pg.pm @@ -32,215 +32,227 @@ use DBD::Pg; # This module extends the DB interface via inheritance use parent qw(Bugzilla::DB); -use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA }; +use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA}; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; - # The default database name for PostgreSQL. We have - # to connect to SOME database, even if we have - # no $dbname parameter. - $dbname ||= 'template1'; + # The default database name for PostgreSQL. We have + # to connect to SOME database, even if we have + # no $dbname parameter. + $dbname ||= 'template1'; - # construct the DSN from the parameters we got - my $dsn = "dbi:Pg:dbname=$dbname"; - $dsn .= ";host=$host" if $host; - $dsn .= ";port=$port" if $port; + # construct the DSN from the parameters we got + my $dsn = "dbi:Pg:dbname=$dbname"; + $dsn .= ";host=$host" if $host; + $dsn .= ";port=$port" if $port; - # This stops Pg from printing out lots of "NOTICE" messages when - # creating tables. - $dsn .= ";options='-c client_min_messages=warning'"; + # This stops Pg from printing out lots of "NOTICE" messages when + # creating tables. + $dsn .= ";options='-c client_min_messages=warning'"; - my $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} }; + my $attrs = {pg_enable_utf8 => Bugzilla->params->{'utf8'}}; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; - bless ($self, $class); + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; - return $self; + bless($self, $class); + + return $self; } # if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version # supported by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_seq"; - my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); + my $seq = $table . "_" . $column . "_seq"; + my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $text, $separator, $sort, $order_by) = @_; - $sort = 1 if !defined $sort; - $separator = $self->quote(', ') if !defined $separator; - - # PostgreSQL 8.x doesn't support STRING_AGG - if (vers_cmp($self->bz_server_version, 9) < 0) { - my $sql = "ARRAY_ACCUM($text)"; - if ($sort) { - $sql = "ARRAY_SORT($sql)"; - } - return "ARRAY_TO_STRING($sql, $separator)"; - } - - if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { - # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x - # ORDER BY y", we need to sort the list, and then get the unique - # values - return "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; - } - - # Determine the ORDER BY clause (if any) - if ($order_by) { - $order_by = " ORDER BY $order_by"; - } - elsif ($sort) { - # We don't include the DISTINCT keyword in an order by - $text =~ /^(?:DISTINCT\s*)?(.+)$/i; - $order_by = " ORDER BY $1"; + my ($self, $text, $separator, $sort, $order_by) = @_; + $sort = 1 if !defined $sort; + $separator = $self->quote(', ') if !defined $separator; + + # PostgreSQL 8.x doesn't support STRING_AGG + if (vers_cmp($self->bz_server_version, 9) < 0) { + my $sql = "ARRAY_ACCUM($text)"; + if ($sort) { + $sql = "ARRAY_SORT($sql)"; } - - return "STRING_AGG(${text}::text, $separator${order_by}::text)" + return "ARRAY_TO_STRING($sql, $separator)"; + } + + if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { + + # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x + # ORDER BY y", we need to sort the list, and then get the unique + # values + return + "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; + } + + # Determine the ORDER BY clause (if any) + if ($order_by) { + $order_by = " ORDER BY $order_by"; + } + elsif ($sort) { + + # We don't include the DISTINCT keyword in an order by + $text =~ /^(?:DISTINCT\s*)?(.+)$/i; + $order_by = " ORDER BY $1"; + } + + return "STRING_AGG(${text}::text, $separator${order_by}::text)"; } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER(${string}::text)"; + return "LOWER(${string}::text)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION(${fragment}::text IN ${text}::text)"; + return "POSITION(${fragment}::text IN ${text}::text)"; } sub sql_like { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not LIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not LIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_ilike { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not ILIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not ILIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_not_ilike { - return shift->sql_ilike(@_, 'NOT'); + return shift->sql_ilike(@_, 'NOT'); } # Escapes any % or _ characters which are special in a LIKE match. # Also performs a $dbh->quote to escape any quote characters. sub sql_like_escape { - my ($self, $fragment) = @_; + my ($self, $fragment) = @_; - $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears - $fragment =~ s/%/\|%/g; # percent and underscore are the special match - $fragment =~ s/_/\|_/g; # characters in SQL. + $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears + $fragment =~ s/%/\|%/g; # percent and underscore are the special match + $fragment =~ s/_/\|_/g; # characters in SQL. - return $self->quote("%$fragment%"); + return $self->quote("%$fragment%"); } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text ~* $pattern"; + return "${expr}::text ~* $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text !~* $pattern" + return "${expr}::text !~* $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "TO_TIMESTAMP('$days', 'J')::date"; + return "TO_TIMESTAMP('$days', 'J')::date"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_CHAR(${date}::date, 'J')::int"; + return "TO_CHAR(${date}::date, 'J')::int"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; - - return "TO_CHAR($date, " . $self->quote($format) . ")"; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; + + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; + + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator $interval * INTERVAL '1 $units'"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator $interval * INTERVAL '1 $units'"; } sub sql_string_concat { - my ($self, @params) = @_; - - # Postgres 7.3 does not support concatenating of different types, so we - # need to cast both parameters to text. Version 7.4 seems to handle this - # properly, so when we stop support 7.3, this can be removed. - return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; + my ($self, @params) = @_; + + # Postgres 7.3 does not support concatenating of different types, so we + # need to cast both parameters to text. Version 7.4 seems to handle this + # properly, so when we stop support 7.3, this can be removed. + return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; } # Tell us whether or not a particular sequence exists in the DB. sub bz_sequence_exists { - my ($self, $seq_name) = @_; - my $exists = $self->selectrow_array( - 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', - undef, $seq_name); - return $exists || 0; + my ($self, $seq_name) = @_; + my $exists + = $self->selectrow_array( + 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', + undef, $seq_name); + return $exists || 0; } sub bz_explain { - my ($self, $sql) = @_; - my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); - return join("\n", @$explain); + my ($self, $sql) = @_; + my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); + return join("\n", @$explain); } ##################################################################### @@ -248,42 +260,49 @@ sub bz_explain { ##################################################################### sub bz_check_server_version { - my $self = shift; - my ($db) = @_; - my $server_version = $self->SUPER::bz_check_server_version(@_); - my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; - # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. - # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. - if ($major_version >= 9) { - local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; - local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; - Bugzilla::DB::_bz_check_dbd(@_); - } + my $self = shift; + my ($db) = @_; + my $server_version = $self->SUPER::bz_check_server_version(@_); + my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; + + # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. + # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. + if ($major_version >= 9) { + local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; + local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; + Bugzilla::DB::_bz_check_dbd(@_); + } } sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - my ($has_plpgsql) = $self->selectrow_array("SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); - $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; - - if (vers_cmp($self->bz_server_version, 9) < 0) { - # Custom Functions for Postgres 8 - my $function = 'array_accum'; - my $array_accum = $self->selectrow_array( - 'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function); - if (!$array_accum) { - print "Creating function $function...\n"; - $self->do("CREATE AGGREGATE array_accum ( + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + my ($has_plpgsql) + = $self->selectrow_array( + "SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); + $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; + + if (vers_cmp($self->bz_server_version, 9) < 0) { + + # Custom Functions for Postgres 8 + my $function = 'array_accum'; + my $array_accum + = $self->selectrow_array('SELECT 1 FROM pg_proc WHERE proname = ?', + undef, $function); + if (!$array_accum) { + print "Creating function $function...\n"; + $self->do( + "CREATE AGGREGATE array_accum ( SFUNC = array_append, BASETYPE = anyelement, STYPE = anyarray, INITCOND = '{}' - )"); - } + )" + ); + } - $self->do(<<'END'); + $self->do(<<'END'); CREATE OR REPLACE FUNCTION array_sort(ANYARRAY) RETURNS ANYARRAY LANGUAGE SQL IMMUTABLE STRICT @@ -296,31 +315,32 @@ SELECT ARRAY( ); $$; END - } - else { - # Custom functions for Postgres 9.0+ - - # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC - # JDBurnZ: https://github.com/JDBurnZ - # Message In Action: https://www.messageinaction.com - # - #Permission is hereby granted, free of charge, to any person obtaining a copy of - #this software and associated documentation files (the "Software"), to deal in - #the Software without restriction, including without limitation the rights to - #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - #the Software, and to permit persons to whom the Software is furnished to do so, - #subject to the following conditions: - # - #The above copyright notice and this permission notice shall be included in all - #copies or substantial portions of the Software. - # - #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - $self->do(q| + } + else { + # Custom functions for Postgres 9.0+ + + # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC + # JDBurnZ: https://github.com/JDBurnZ + # Message In Action: https://www.messageinaction.com + # + #Permission is hereby granted, free of charge, to any person obtaining a copy of + #this software and associated documentation files (the "Software"), to deal in + #the Software without restriction, including without limitation the rights to + #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + #the Software, and to permit persons to whom the Software is furnished to do so, + #subject to the following conditions: + # + #The above copyright notice and this permission notice shall be included in all + #copies or substantial portions of the Software. + # + #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + $self->do( + q| DROP FUNCTION IF EXISTS anyarray_uniq(anyarray); CREATE OR REPLACE FUNCTION anyarray_uniq(with_array anyarray) RETURNS anyarray AS $BODY$ @@ -345,135 +365,152 @@ END RETURN return_array; END; $BODY$ LANGUAGE plpgsql; - |); + | + ); + } + + # PostgreSQL doesn't like having *any* index on the thetext + # field, because it can't have index data longer than 2770 + # characters on that field. + $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + + # Same for all the comments fields in the fulltext table. + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_noprivate_idx'); + + # PostgreSQL also wants an index for calling LOWER on + # login_name, which we do with sql_istrcmp all over the place. + $self->bz_add_index( + 'profiles', + 'profiles_login_name_lower_idx', + {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'} + ); + + # Now that Bugzilla::Object uses sql_istrcmp, other tables + # also need a LOWER() index. + _fix_case_differences('fielddefs', 'name'); + $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('keyworddefs', 'name'); + $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('products', 'name'); + $self->bz_add_index('products', 'products_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + + # bz_rename_column and bz_rename_table didn't correctly rename + # the sequence. + $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', + 'fielddefs_id_seq'); + + # If the 'tags' table still exists, then bz_rename_table() + # will fix the sequence for us. + if (!$self->bz_table_info('tags')) { + my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); + + # If $res is true, then the sequence has been renamed, meaning that + # the primary key must be renamed too. + if ($res) { + $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); } - - # PostgreSQL doesn't like having *any* index on the thetext - # field, because it can't have index data longer than 2770 - # characters on that field. - $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - # Same for all the comments fields in the fulltext table. - $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); - $self->bz_drop_index('bugs_fulltext', - 'bugs_fulltext_comments_noprivate_idx'); - - # PostgreSQL also wants an index for calling LOWER on - # login_name, which we do with sql_istrcmp all over the place. - $self->bz_add_index('profiles', 'profiles_login_name_lower_idx', - {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'}); - - # Now that Bugzilla::Object uses sql_istrcmp, other tables - # also need a LOWER() index. - _fix_case_differences('fielddefs', 'name'); - $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('keyworddefs', 'name'); - $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('products', 'name'); - $self->bz_add_index('products', 'products_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - - # bz_rename_column and bz_rename_table didn't correctly rename - # the sequence. - $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', 'fielddefs_id_seq'); - # If the 'tags' table still exists, then bz_rename_table() - # will fix the sequence for us. - if (!$self->bz_table_info('tags')) { - my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); - # If $res is true, then the sequence has been renamed, meaning that - # the primary key must be renamed too. - if ($res) { - $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); - } - } - - # Certain sequences got upgraded before we required Pg 8.3, and - # so they were not properly associated with their columns. - my @tables = $self->bz_table_list_real; - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - # All our SERIAL pks have "id" in their name at the end. - next unless $column =~ /id$/; - my $sequence = "${table}_${column}_seq"; - if ($self->bz_sequence_exists($sequence)) { - my $is_associated = $self->selectrow_array( - 'SELECT pg_get_serial_sequence(?,?)', - undef, $table, $column); - next if $is_associated; - print "Fixing $sequence to be associated" - . " with $table.$column...\n"; - $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); - # In order to produce an exactly identical schema to what - # a brand-new checksetup.pl run would produce, we also need - # to re-set the default on this column. - $self->do("ALTER TABLE $table + } + + # Certain sequences got upgraded before we required Pg 8.3, and + # so they were not properly associated with their columns. + my @tables = $self->bz_table_list_real; + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + + # All our SERIAL pks have "id" in their name at the end. + next unless $column =~ /id$/; + my $sequence = "${table}_${column}_seq"; + if ($self->bz_sequence_exists($sequence)) { + my $is_associated = $self->selectrow_array('SELECT pg_get_serial_sequence(?,?)', + undef, $table, $column); + next if $is_associated; + print "Fixing $sequence to be associated" . " with $table.$column...\n"; + $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); + + # In order to produce an exactly identical schema to what + # a brand-new checksetup.pl run would produce, we also need + # to re-set the default on this column. + $self->do( + "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('$sequence')"); - } - } + SET DEFAULT nextval('$sequence')" + ); + } } + } } sub _fix_bad_sequence { - my ($self, $table, $column, $old_seq, $new_seq) = @_; - if ($self->bz_column_info($table, $column) - && $self->bz_sequence_exists($old_seq)) - { - print "Fixing $old_seq sequence...\n"; - $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - $self->do("ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - return 1; - } - return 0; + my ($self, $table, $column, $old_seq, $new_seq) = @_; + if ( $self->bz_column_info($table, $column) + && $self->bz_sequence_exists($old_seq)) + { + print "Fixing $old_seq sequence...\n"; + $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + $self->do( + "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); + return 1; + } + return 0; } # Renames things that differ only in case. sub _fix_case_differences { - my ($table, $field) = @_; - my $dbh = Bugzilla->dbh; - - my $duplicates = $dbh->selectcol_arrayref( - "SELECT DISTINCT LOWER($field) FROM $table - GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1"); - - foreach my $name (@$duplicates) { - my $dups = $dbh->selectcol_arrayref( - "SELECT $field FROM $table WHERE LOWER($field) = ?", - undef, $name); - my $primary = shift @$dups; - foreach my $dup (@$dups) { - my $new_name = "${dup}_"; - # Make sure the new name isn't *also* a duplicate. - while (1) { - last if (!$dbh->selectrow_array( - "SELECT 1 FROM $table WHERE LOWER($field) = ?", - undef, lc($new_name))); - $new_name .= "_"; - } - print "$table '$primary' and '$dup' have names that differ", - " only in case.\nRenaming '$dup' to '$new_name'...\n"; - $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", - undef, $new_name, $dup); - } + my ($table, $field) = @_; + my $dbh = Bugzilla->dbh; + + my $duplicates = $dbh->selectcol_arrayref( + "SELECT DISTINCT LOWER($field) FROM $table + GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1" + ); + + foreach my $name (@$duplicates) { + my $dups + = $dbh->selectcol_arrayref( + "SELECT $field FROM $table WHERE LOWER($field) = ?", + undef, $name); + my $primary = shift @$dups; + foreach my $dup (@$dups) { + my $new_name = "${dup}_"; + + # Make sure the new name isn't *also* a duplicate. + while (1) { + last + if (!$dbh->selectrow_array( + "SELECT 1 FROM $table WHERE LOWER($field) = ?", + undef, lc($new_name) + )); + $new_name .= "_"; + } + print "$table '$primary' and '$dup' have names that differ", + " only in case.\nRenaming '$dup' to '$new_name'...\n"; + $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", + undef, $new_name, $dup); } + } } ##################################################################### # Custom Schema Information Functions ##################################################################### -# Pg includes the PostgreSQL system tables in table_list_real, so +# Pg includes the PostgreSQL system tables in table_list_real, so # we need to remove those. sub bz_table_list_real { - my $self = shift; + my $self = shift; + + my @full_table_list = $self->SUPER::bz_table_list_real(@_); - my @full_table_list = $self->SUPER::bz_table_list_real(@_); - # All PostgreSQL system tables start with "pg_" or "sql_" - my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); - return @table_list; + # All PostgreSQL system tables start with "pg_" or "sql_" + my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); + return @table_list; } 1; diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index d1c1dc7e9..94b8734f3 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -29,6 +29,7 @@ use Digest::MD5 qw(md5_hex); use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys); use List::MoreUtils qw(firstidx natatime); use Safe; + # Historical, needed for SCHEMA_VERSION = '1.00' use Storable qw(dclone freeze thaw); @@ -197,1596 +198,1544 @@ update this column in this table." =cut -use constant SCHEMA_VERSION => 3; -use constant ADD_COLUMN => 'ADD COLUMN'; +use constant SCHEMA_VERSION => 3; +use constant ADD_COLUMN => 'ADD COLUMN'; + # Multiple FKs can be added using ALTER TABLE ADD CONSTRAINT in one # SQL statement. This isn't true for all databases. use constant MULTIPLE_FKS_IN_ALTER => 1; + # This is a reasonable default that's true for both PostgreSQL and MySQL. use constant MAX_IDENTIFIER_LEN => 63; use constant FIELD_TABLE_SCHEMA => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + visibility_value_id => {TYPE => 'INT2'}, + ], + + # Note that bz_add_field_table should prepend the table name + # to these index names. + INDEXES => [ + value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + sortkey_idx => ['sortkey', 'value'], + visibility_value_id_idx => ['visibility_value_id'], + ], +}; + +use constant ABSTRACT_SCHEMA => { + + # BUG-RELATED TABLES + # ------------------ + + # General Bug Information + # ----------------------- + bugs => { FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - visibility_value_id => {TYPE => 'INT2'}, + bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + assigned_to => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_file_loc => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, + bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, + creation_ts => {TYPE => 'DATETIME'}, + delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + op_sys => {TYPE => 'varchar(64)', NOTNULL => 1}, + priority => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id'} + }, + rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, + reporter => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + version => {TYPE => 'varchar(64)', NOTNULL => 1}, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id'} + }, + resolution => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + target_milestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + qa_contact => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + lastdiffed => {TYPE => 'DATETIME'}, + everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, + reporter_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + cclist_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + estimated_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + remaining_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + deadline => {TYPE => 'DATETIME'}, ], - # Note that bz_add_field_table should prepend the table name - # to these index names. INDEXES => [ - value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, - sortkey_idx => ['sortkey', 'value'], - visibility_value_id_idx => ['visibility_value_id'], + bugs_assigned_to_idx => ['assigned_to'], + bugs_creation_ts_idx => ['creation_ts'], + bugs_delta_ts_idx => ['delta_ts'], + bugs_bug_severity_idx => ['bug_severity'], + bugs_bug_status_idx => ['bug_status'], + bugs_op_sys_idx => ['op_sys'], + bugs_priority_idx => ['priority'], + bugs_product_id_idx => ['product_id'], + bugs_reporter_idx => ['reporter'], + bugs_version_idx => ['version'], + bugs_component_id_idx => ['component_id'], + bugs_resolution_idx => ['resolution'], + bugs_target_milestone_idx => ['target_milestone'], + bugs_qa_contact_idx => ['qa_contact'], ], -}; + }, -use constant ABSTRACT_SCHEMA => { + bugs_fulltext => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + + # Comments are stored all together in one column for searching. + # This allows us to examine all comments together when deciding + # the relevance of a bug in fulltext search. + comments => {TYPE => 'LONGTEXT'}, + comments_noprivate => {TYPE => 'LONGTEXT'}, + ], + INDEXES => [ + bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_idx => {FIELDS => ['comments'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_noprivate_idx => + {FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, + ], + }, - # BUG-RELATED TABLES - # ------------------ - - # General Bug Information - # ----------------------- - bugs => { - FIELDS => [ - bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - assigned_to => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_file_loc => {TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, - bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, - creation_ts => {TYPE => 'DATETIME'}, - delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - op_sys => {TYPE => 'varchar(64)', NOTNULL => 1}, - priority => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id'}}, - rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, - reporter => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - version => {TYPE => 'varchar(64)', NOTNULL => 1}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id'}}, - resolution => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "''"}, - target_milestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - qa_contact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - lastdiffed => {TYPE => 'DATETIME'}, - everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, - reporter_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - cclist_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - estimated_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - remaining_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - deadline => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - bugs_assigned_to_idx => ['assigned_to'], - bugs_creation_ts_idx => ['creation_ts'], - bugs_delta_ts_idx => ['delta_ts'], - bugs_bug_severity_idx => ['bug_severity'], - bugs_bug_status_idx => ['bug_status'], - bugs_op_sys_idx => ['op_sys'], - bugs_priority_idx => ['priority'], - bugs_product_id_idx => ['product_id'], - bugs_reporter_idx => ['reporter'], - bugs_version_idx => ['version'], - bugs_component_id_idx => ['component_id'], - bugs_resolution_idx => ['resolution'], - bugs_target_milestone_idx => ['target_milestone'], - bugs_qa_contact_idx => ['qa_contact'], - ], - }, - - bugs_fulltext => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - # Comments are stored all together in one column for searching. - # This allows us to examine all comments together when deciding - # the relevance of a bug in fulltext search. - comments => {TYPE => 'LONGTEXT'}, - comments_noprivate => {TYPE => 'LONGTEXT'}, - ], - INDEXES => [ - bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_idx => {FIELDS => ['comments'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_noprivate_idx => { - FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, - ], - }, - - bugs_activity => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - added => {TYPE => 'varchar(255)'}, - removed => {TYPE => 'varchar(255)'}, - comment_id => {TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_activity_bug_id_idx => ['bug_id'], - bugs_activity_who_idx => ['who'], - bugs_activity_bug_when_idx => ['bug_when'], - bugs_activity_fieldid_idx => ['fieldid'], - bugs_activity_added_idx => ['added'], - bugs_activity_removed_idx => ['removed'], - ], - }, - - bugs_aliases => { - FIELDS => [ - alias => {TYPE => 'varchar(40)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_aliases_bug_id_idx => ['bug_id'], - bugs_aliases_alias_idx => {FIELDS => ['alias'], - TYPE => 'UNIQUE'}, - ], - }, - - cc => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - cc_bug_id_idx => {FIELDS => [qw(bug_id who)], - TYPE => 'UNIQUE'}, - cc_who_idx => ['who'], - ], - }, - - longdescs => { - FIELDS => [ - comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, - DEFAULT => '0'}, - thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - extra_data => {TYPE => 'varchar(255)'} - ], - INDEXES => [ - longdescs_bug_id_idx => [qw(bug_id work_time)], - longdescs_who_idx => [qw(who bug_id)], - longdescs_bug_when_idx => ['bug_when'], - ], - }, - - longdescs_tags => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_weights => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - weight => { TYPE => 'INT3', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_activity => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - bug_id => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' }}, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - who => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid' }}, - bug_when => { TYPE => 'DATETIME', NOTNULL => 1 }, - added => { TYPE => 'varchar(24)' }, - removed => { TYPE => 'varchar(24)' }, - ], - INDEXES => [ - longdescs_tags_activity_bug_id_idx => ['bug_id'], - ], - }, - - dependencies => { - FIELDS => [ - blocked => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dependson => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)], - TYPE => 'UNIQUE'}, - dependencies_dependson_idx => ['dependson'], - ], - }, - - attachments => { - FIELDS => [ - attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, - ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - filename => {TYPE => 'varchar(255)', NOTNULL => 1}, - submitter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - attachments_bug_id_idx => ['bug_id'], - attachments_creation_ts_idx => ['creation_ts'], - attachments_modification_time_idx => ['modification_time'], - attachments_submitter_id_idx => ['submitter_id', 'bug_id'], - ], - }, - attach_data => { - FIELDS => [ - id => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - duplicates => { - FIELDS => [ - dupe_of => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dupe => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - }, - - bug_see_also => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(255)', NOTNULL => 1}, - class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - ], - INDEXES => [ - bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Auditing - # -------- - - audit_log => { - FIELDS => [ - user_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - class => {TYPE => 'varchar(255)', NOTNULL => 1}, - object_id => {TYPE => 'INT4', NOTNULL => 1}, - field => {TYPE => 'varchar(64)', NOTNULL => 1}, - removed => {TYPE => 'MEDIUMTEXT'}, - added => {TYPE => 'MEDIUMTEXT'}, - at_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - audit_log_class_idx => ['class', 'at_time'], - ], - }, - - # Keywords - # -------- - - keyworddefs => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - ], - INDEXES => [ - keyworddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - keywords => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - keywordid => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'keyworddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - - ], - INDEXES => [ - keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], - TYPE => 'UNIQUE'}, - keywords_keywordid_idx => ['keywordid'], - ], - }, - - # Flags - # ----- - - # "flags" stores one record for each flag on each bug/attachment. - flags => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - status => {TYPE => 'char(1)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_date => {TYPE => 'DATETIME'}, - setter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - requestee_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - ], - INDEXES => [ - flags_bug_id_idx => [qw(bug_id attach_id)], - flags_setter_id_idx => ['setter_id'], - flags_requestee_id_idx => ['requestee_id'], - flags_type_id_idx => ['type_id'], - ], - }, - - # "flagtypes" defines the types of flags that can be set. - flagtypes => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(50)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - cc_list => {TYPE => 'varchar(200)'}, - target_type => {TYPE => 'char(1)', NOTNULL => 1, - DEFAULT => "'b'"}, - is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - grant_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - request_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - ], - }, - - # "flaginclusions" and "flagexclusions" specify the products/components - # a bug/attachment must belong to in order for flags of a given type - # to be set for them. - flaginclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - flagexclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - # General Field Information - # ------------------------- - - fielddefs => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => FIELD_TYPE_UNKNOWN}, - custom => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1}, - obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - visibility_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - value_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - reverse_desc => {TYPE => 'TINYTEXT'}, - is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - fielddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - fielddefs_sortkey_idx => ['sortkey'], - fielddefs_value_field_id_idx => ['value_field_id'], - fielddefs_is_mandatory_idx => ['is_mandatory'], - ], - }, - - # Field Visibility Information - # ------------------------- - - field_visibility => { - FIELDS => [ - field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value_id => {TYPE => 'INT2', NOTNULL => 1} - ], - INDEXES => [ - field_visibility_field_id_idx => { - FIELDS => [qw(field_id value_id)], - TYPE => 'UNIQUE' - }, - ], - }, - - # Per-product Field Values - # ------------------------ - - versions => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - versions_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - milestones => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - milestones_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Global Field Values - # ------------------- - - bug_status => { - FIELDS => [ - @{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) }, - is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, - - ], - INDEXES => [ - bug_status_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_status_sortkey_idx => ['sortkey', 'value'], - bug_status_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - resolution => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - resolution_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - resolution_sortkey_idx => ['sortkey', 'value'], - resolution_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - bug_severity => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - bug_severity_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_severity_sortkey_idx => ['sortkey', 'value'], - bug_severity_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - priority => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - priority_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - priority_sortkey_idx => ['sortkey', 'value'], - priority_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - rep_platform => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - rep_platform_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - rep_platform_sortkey_idx => ['sortkey', 'value'], - rep_platform_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - op_sys => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - op_sys_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - op_sys_sortkey_idx => ['sortkey', 'value'], - op_sys_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - status_workflow => { - FIELDS => [ - # On bug creation, there is no old value. - old_status => {TYPE => 'INT2', - REFERENCES => {TABLE => 'bug_status', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - new_status => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'bug_status', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - status_workflow_idx => {FIELDS => ['old_status', 'new_status'], - TYPE => 'UNIQUE'}, - ], - }, - - # USER INFO - # --------- - - # General User Information - # ------------------------ - - profiles => { - FIELDS => [ - userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, - cryptpassword => {TYPE => 'varchar(128)'}, - realname => {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, - disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - extern_id => {TYPE => 'varchar(64)'}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - last_seen_date => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - profiles_login_name_idx => {FIELDS => ['login_name'], - TYPE => 'UNIQUE'}, - profiles_extern_id_idx => {FIELDS => ['extern_id'], - TYPE => 'UNIQUE'} - ], - }, - - profile_search => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - list_order => {TYPE => 'MEDIUMTEXT'}, - ], - INDEXES => [ - profile_search_user_id_idx => [qw(user_id)], - ], - }, - - profiles_activity => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - oldvalue => {TYPE => 'TINYTEXT'}, - newvalue => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - profiles_activity_userid_idx => ['userid'], - profiles_activity_profiles_when_idx => ['profiles_when'], - profiles_activity_fieldid_idx => ['fieldid'], - ], - }, - - email_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - relationship => {TYPE => 'INT1', NOTNULL => 1}, - event => {TYPE => 'INT1', NOTNULL => 1}, - ], - INDEXES => [ - email_setting_user_id_idx => - {FIELDS => [qw(user_id relationship event)], - TYPE => 'UNIQUE'}, - ], - }, - - email_bug_ignore => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)], - TYPE => 'UNIQUE'}, - ], - }, - - watch => { - FIELDS => [ - watcher => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - watched => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - watch_watcher_idx => {FIELDS => [qw(watcher watched)], - TYPE => 'UNIQUE'}, - watch_watched_idx => ['watched'], - ], - }, - - namedqueries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - namedqueries_userid_idx => {FIELDS => [qw(userid name)], - TYPE => 'UNIQUE'}, - ], - }, - - namedqueries_link_in_footer => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)], - TYPE => 'UNIQUE'}, - namedqueries_link_in_footer_userid_idx => ['user_id'], - ], - }, - - tag => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}, - ], - }, - - bug_tag => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - tag_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'tag', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'}, - ], - }, - - reports => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - reports_user_id_idx => {FIELDS => [qw(user_id name)], - TYPE => 'UNIQUE'}, - ], - }, - - component_cc => { - - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)], - TYPE => 'UNIQUE'}, - ], - }, - - # Authentication - # -------------- - - logincookies => { - FIELDS => [ - cookie => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ipaddr => {TYPE => 'varchar(40)'}, - lastused => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - logincookies_lastused_idx => ['lastused'], - ], - }, - - login_failure => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - login_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1}, - ], - INDEXES => [ - # We do lookups by every item in the table simultaneously, but - # having an index with all three items would be the same size as - # the table. So instead we have an index on just the smallest item, - # to speed lookups. - login_failure_user_id_idx => ['user_id'], - ], - }, - - - # "tokens" stores the tokens users receive when a password or email - # change is requested. Tokens provide an extra measure of security - # for these changes. - tokens => { - FIELDS => [ - userid => {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - issuedate => {TYPE => 'DATETIME', NOTNULL => 1} , - token => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} , - eventdata => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - tokens_userid_idx => ['userid'], - ], - }, - - # GROUPS - # ------ - - groups => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(255)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, - userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, - DEFAULT => "''"}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - icon_url => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, - ], - }, - - group_control_map => { - FIELDS => [ - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - entry => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - membercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - othercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - group_control_map_product_id_idx => - {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, - group_control_map_group_id_idx => ['group_id'], - ], - }, - - # "user_group_map" determines the groups that a user belongs to - # directly or due to regexp and which groups can be blessed by a user. - # - # grant_type: - # if GRANT_DIRECT - record was explicitly granted - # if GRANT_DERIVED - record was derived from expanding a group hierarchy - # if GRANT_REGEXP - record was created by evaluating a regexp - user_group_map => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GRANT_DIRECT}, - ], - INDEXES => [ - user_group_map_user_id_idx => - {FIELDS => [qw(user_id group_id grant_type isbless)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups are made a member of another - # group, given the ability to bless another group, or given - # visibility to another groups existence and membership - # grant_type: - # if GROUP_MEMBERSHIP - member groups are made members of grantor - # if GROUP_BLESS - member groups may grant membership in grantor - # if GROUP_VISIBLE - member groups may see grantor group - group_group_map => { - FIELDS => [ - member_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grantor_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GROUP_MEMBERSHIP}, - ], - INDEXES => [ - group_group_map_member_id_idx => - {FIELDS => [qw(member_id grantor_id grant_type)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a bug. - bug_group_map => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_group_map_bug_id_idx => - {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, - bug_group_map_group_id_idx => ['group_id'], - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a named query somebody else shares. - namedquery_group_map => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedquery_group_map_namedquery_id_idx => - {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, - namedquery_group_map_group_id_idx => ['group_id'], - ], - }, - - category_group_map => { - FIELDS => [ - category_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - category_group_map_category_id_idx => - {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, - ], - }, - - - # PRODUCTS - # -------- - - classifications => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - classifications_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - products => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - classification_id => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '1', - REFERENCES => {TABLE => 'classifications', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 1}, - defaultmilestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - products_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - components => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - initialowner => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - initialqacontact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - components_product_id_idx => {FIELDS => [qw(product_id name)], - TYPE => 'UNIQUE'}, - components_name_idx => ['name'], - ], - }, - - # CHARTS - # ------ - - series => { - FIELDS => [ - series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - creator => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - category => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - subcategory => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - frequency => {TYPE => 'INT2', NOTNULL => 1}, - query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - series_creator_idx => ['creator'], - series_category_idx => {FIELDS => [qw(category subcategory name)], - TYPE => 'UNIQUE'}, - ], - }, - - series_data => { - FIELDS => [ - series_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'series', - COLUMN => 'series_id', - DELETE => 'CASCADE'}}, - series_date => {TYPE => 'DATETIME', NOTNULL => 1}, - series_value => {TYPE => 'INT3', NOTNULL => 1}, - ], - INDEXES => [ - series_data_series_id_idx => - {FIELDS => [qw(series_id series_date)], - TYPE => 'UNIQUE'}, - ], - }, - - series_categories => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - ], - INDEXES => [ - series_categories_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - # WHINE SYSTEM - # ------------ - - whine_queries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - query_name => {TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "''"}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - title => {TYPE => 'varchar(128)', NOTNULL => 1, - DEFAULT => "''"}, - ], - INDEXES => [ - whine_queries_eventid_idx => ['eventid'], - ], - }, - - whine_schedules => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - run_day => {TYPE => 'varchar(32)'}, - run_time => {TYPE => 'varchar(32)'}, - run_next => {TYPE => 'DATETIME'}, - mailto => {TYPE => 'INT3', NOTNULL => 1}, - mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - whine_schedules_run_next_idx => ['run_next'], - whine_schedules_eventid_idx => ['eventid'], - ], - }, - - whine_events => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - owner_userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - subject => {TYPE => 'varchar(128)'}, - body => {TYPE => 'MEDIUMTEXT'}, - mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - }, - - # QUIPS - # ----- - - quips => { - FIELDS => [ - quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - quip => {TYPE => 'varchar(512)', NOTNULL => 1}, - approved => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - }, - - # SETTINGS - # -------- - # setting - each global setting will have exactly one entry - # in this table. - # setting_value - stores the list of acceptable values for each - # setting, and a sort index that controls the order - # in which the values are displayed. - # profile_setting - If a user has chosen to use a value other than the - # global default for a given setting, it will be - # stored in this table. Note: even if a setting is - # later changed so is_enabled = false, the stored - # value will remain in case it is ever enabled again. - # - setting => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - PRIMARYKEY => 1}, - default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - subclass => {TYPE => 'varchar(32)'}, - ], - }, - - setting_value => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(32)', NOTNULL => 1}, - sortindex => {TYPE => 'INT2', NOTNULL => 1}, - ], - INDEXES => [ - setting_value_nv_unique_idx => {FIELDS => [qw(name value)], - TYPE => 'UNIQUE'}, - setting_value_ns_unique_idx => {FIELDS => [qw(name sortindex)], - TYPE => 'UNIQUE'}, - ], - }, - - profile_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - setting_name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - ], - INDEXES => [ - profile_setting_value_unique_idx => {FIELDS => [qw(user_id setting_name)], - TYPE => 'UNIQUE'}, - ], - }, - - # BUGMAIL - # ------- - - mail_staging => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - message => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - # THESCHWARTZ TABLES - # ------------------ - # Note: In the standard TheSchwartz schema, most integers are unsigned, - # but we didn't implement unsigned ints for Bugzilla schemas, so we - # just create signed ints, which should be fine. - - ts_funcmap => { - FIELDS => [ - funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, - ], - INDEXES => [ - ts_funcmap_funcname_idx => {FIELDS => ['funcname'], - TYPE => 'UNIQUE'}, - ], - }, - - ts_job => { - FIELDS => [ - # In a standard TheSchwartz schema, this is a BIGINT, but we - # don't have those and I didn't want to add them just for this. - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1}, - # In standard TheSchwartz, this is a MEDIUMBLOB. - arg => {TYPE => 'LONGBLOB'}, - uniqkey => {TYPE => 'varchar(255)'}, - insert_time => {TYPE => 'INT4'}, - run_after => {TYPE => 'INT4', NOTNULL => 1}, - grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, - priority => {TYPE => 'INT2'}, - coalesce => {TYPE => 'varchar(255)'}, - ], - INDEXES => [ - ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], - TYPE => 'UNIQUE'}, - # In a standard TheSchewartz schema, these both go in the other - # direction, but there's no reason to have three indexes that - # all start with the same column, and our naming scheme doesn't - # allow it anyhow. - ts_job_run_after_idx => [qw(run_after funcid)], - ts_job_coalesce_idx => [qw(coalesce funcid)], - ], - }, - - ts_note => { - FIELDS => [ - # This is a BIGINT in standard TheSchwartz schemas. - jobid => {TYPE => 'INT4', NOTNULL => 1}, - notekey => {TYPE => 'varchar(255)'}, - value => {TYPE => 'LONGBLOB'}, - ], - INDEXES => [ - ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], - TYPE => 'UNIQUE'}, - ], - }, - - ts_error => { - FIELDS => [ - error_time => {TYPE => 'INT4', NOTNULL => 1}, - jobid => {TYPE => 'INT4', NOTNULL => 1}, - message => {TYPE => 'varchar(255)', NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - ts_error_funcid_idx => [qw(funcid error_time)], - ts_error_error_time_idx => ['error_time'], - ts_error_jobid_idx => ['jobid'], - ], - }, - - ts_exitstatus => { - FIELDS => [ - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - status => {TYPE => 'INT2'}, - completion_time => {TYPE => 'INT4'}, - delete_after => {TYPE => 'INT4'}, - ], - INDEXES => [ - ts_exitstatus_funcid_idx => ['funcid'], - ts_exitstatus_delete_after_idx => ['delete_after'], - ], - }, - - # SCHEMA STORAGE - # -------------- - - bz_schema => { - FIELDS => [ - schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, - version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, - ], - }, - - bug_user_last_visit => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], - TYPE => 'UNIQUE'}, - bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], - ], - }, - - user_api_keys => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, - description => {TYPE => 'VARCHAR(255)'}, - revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - last_used => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, - user_api_keys_user_id_idx => ['user_id'], - ], - }, -}; + bugs_activity => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + added => {TYPE => 'varchar(255)'}, + removed => {TYPE => 'varchar(255)'}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_activity_bug_id_idx => ['bug_id'], + bugs_activity_who_idx => ['who'], + bugs_activity_bug_when_idx => ['bug_when'], + bugs_activity_fieldid_idx => ['fieldid'], + bugs_activity_added_idx => ['added'], + bugs_activity_removed_idx => ['removed'], + ], + }, -# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables -use constant MULTI_SELECT_VALUE_TABLE => { + bugs_aliases => { + FIELDS => [ + alias => {TYPE => 'varchar(40)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_aliases_bug_id_idx => ['bug_id'], + bugs_aliases_alias_idx => {FIELDS => ['alias'], TYPE => 'UNIQUE'}, + ], + }, + + cc => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + cc_bug_id_idx => {FIELDS => [qw(bug_id who)], TYPE => 'UNIQUE'}, + cc_who_idx => ['who'], + ], + }, + + longdescs => { + FIELDS => [ + comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + extra_data => {TYPE => 'varchar(255)'} + ], + INDEXES => [ + longdescs_bug_id_idx => [qw(bug_id work_time)], + longdescs_who_idx => [qw(who bug_id)], + longdescs_bug_when_idx => ['bug_when'], + ], + }, + + longdescs_tags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_idx => {FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_weights => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + weight => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_weights_tag_idx => {FIELDS => ['tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + added => {TYPE => 'varchar(24)'}, + removed => {TYPE => 'varchar(24)'}, + ], + INDEXES => [longdescs_tags_activity_bug_id_idx => ['bug_id'],], + }, + + dependencies => { + FIELDS => [ + blocked => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dependson => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + dependencies_blocked_idx => + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}, + dependencies_dependson_idx => ['dependson'], + ], + }, + + attachments => { + FIELDS => [ + attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, + ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + filename => {TYPE => 'varchar(255)', NOTNULL => 1}, + submitter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + attachments_bug_id_idx => ['bug_id'], + attachments_creation_ts_idx => ['creation_ts'], + attachments_modification_time_idx => ['modification_time'], + attachments_submitter_id_idx => ['submitter_id', 'bug_id'], + ], + }, + attach_data => { FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, + id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + duplicates => { + FIELDS => [ + dupe_of => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dupe => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + }, + + bug_see_also => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(255)', NOTNULL => 1}, + class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [ + bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Auditing + # -------- + + audit_log => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + class => {TYPE => 'varchar(255)', NOTNULL => 1}, + object_id => {TYPE => 'INT4', NOTNULL => 1}, + field => {TYPE => 'varchar(64)', NOTNULL => 1}, + removed => {TYPE => 'MEDIUMTEXT'}, + added => {TYPE => 'MEDIUMTEXT'}, + at_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [audit_log_class_idx => ['class', 'at_time'],], + }, + + # Keywords + # -------- + + keyworddefs => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [keyworddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + keywords => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + keywordid => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'keyworddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + + ], + INDEXES => [ + keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], TYPE => 'UNIQUE'}, + keywords_keywordid_idx => ['keywordid'], + ], + }, + + # Flags + # ----- + + # "flags" stores one record for each flag on each bug/attachment. + flags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + type_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + status => {TYPE => 'char(1)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_date => {TYPE => 'DATETIME'}, + setter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + requestee_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + ], + INDEXES => [ + flags_bug_id_idx => [qw(bug_id attach_id)], + flags_setter_id_idx => ['setter_id'], + flags_requestee_id_idx => ['requestee_id'], + flags_type_id_idx => ['type_id'], + ], + }, + + # "flagtypes" defines the types of flags that can be set. + flagtypes => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(50)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + cc_list => {TYPE => 'varchar(200)'}, + target_type => {TYPE => 'char(1)', NOTNULL => 1, DEFAULT => "'b'"}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + grant_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + request_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + ], + }, + + # "flaginclusions" and "flagexclusions" specify the products/components + # a bug/attachment must belong to in order for flags of a given type + # to be set for them. + flaginclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, ], INDEXES => [ - bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'}, + flaginclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + flagexclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + flagexclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + # General Field Information + # ------------------------- + + fielddefs => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => FIELD_TYPE_UNKNOWN}, + custom => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1}, + obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + visibility_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + value_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + reverse_desc => {TYPE => 'TINYTEXT'}, + is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + fielddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, + fielddefs_sortkey_idx => ['sortkey'], + fielddefs_value_field_id_idx => ['value_field_id'], + fielddefs_is_mandatory_idx => ['is_mandatory'], + ], + }, + + # Field Visibility Information + # ------------------------- + + field_visibility => { + FIELDS => [ + field_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value_id => {TYPE => 'INT2', NOTNULL => 1} + ], + INDEXES => [ + field_visibility_field_id_idx => + {FIELDS => [qw(field_id value_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Per-product Field Values + # ------------------------ + + versions => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + versions_product_id_idx => {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + milestones => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + milestones_product_id_idx => + {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Global Field Values + # ------------------- + + bug_status => { + FIELDS => [ + @{dclone(FIELD_TABLE_SCHEMA->{FIELDS})}, + is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + + ], + INDEXES => [ + bug_status_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_status_sortkey_idx => ['sortkey', 'value'], + bug_status_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + resolution => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + resolution_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + resolution_sortkey_idx => ['sortkey', 'value'], + resolution_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + bug_severity => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + bug_severity_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_severity_sortkey_idx => ['sortkey', 'value'], + bug_severity_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + priority => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + priority_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + priority_sortkey_idx => ['sortkey', 'value'], + priority_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + rep_platform => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + rep_platform_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + rep_platform_sortkey_idx => ['sortkey', 'value'], + rep_platform_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + op_sys => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + op_sys_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + op_sys_sortkey_idx => ['sortkey', 'value'], + op_sys_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + status_workflow => { + FIELDS => [ + + # On bug creation, there is no old value. + old_status => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'} + }, + new_status => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'} + }, + require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + status_workflow_idx => + {FIELDS => ['old_status', 'new_status'], TYPE => 'UNIQUE'}, + ], + }, + + # USER INFO + # --------- + + # General User Information + # ------------------------ + + profiles => { + FIELDS => [ + userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, + cryptpassword => {TYPE => 'varchar(128)'}, + realname => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + extern_id => {TYPE => 'varchar(64)'}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + last_seen_date => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'}, + profiles_extern_id_idx => {FIELDS => ['extern_id'], TYPE => 'UNIQUE'} + ], + }, + + profile_search => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + list_order => {TYPE => 'MEDIUMTEXT'}, + ], + INDEXES => [profile_search_user_id_idx => [qw(user_id)],], + }, + + profiles_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + oldvalue => {TYPE => 'TINYTEXT'}, + newvalue => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [ + profiles_activity_userid_idx => ['userid'], + profiles_activity_profiles_when_idx => ['profiles_when'], + profiles_activity_fieldid_idx => ['fieldid'], + ], + }, + + email_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + relationship => {TYPE => 'INT1', NOTNULL => 1}, + event => {TYPE => 'INT1', NOTNULL => 1}, + ], + INDEXES => [ + email_setting_user_id_idx => + {FIELDS => [qw(user_id relationship event)], TYPE => 'UNIQUE'}, + ], + }, + + email_bug_ignore => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + email_bug_ignore_user_id_idx => + {FIELDS => [qw(user_id bug_id)], TYPE => 'UNIQUE'}, + ], + }, + + watch => { + FIELDS => [ + watcher => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + watched => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + watch_watcher_idx => {FIELDS => [qw(watcher watched)], TYPE => 'UNIQUE'}, + watch_watched_idx => ['watched'], + ], + }, + + namedqueries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [namedqueries_userid_idx => {FIELDS => [qw(userid name)], TYPE => 'UNIQUE'},], + }, + + namedqueries_link_in_footer => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedqueries_link_in_footer_id_idx => + {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'}, + namedqueries_link_in_footer_userid_idx => ['user_id'], + ], + }, + + tag => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + bug_tag => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + tag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tag', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'},], + }, + + reports => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [reports_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + component_cc => { + + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + component_cc_user_id_idx => + {FIELDS => [qw(component_id user_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Authentication + # -------------- + + logincookies => { + FIELDS => [ + cookie => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ipaddr => {TYPE => 'varchar(40)'}, + lastused => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [logincookies_lastused_idx => ['lastused'],], + }, + + login_failure => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + login_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1}, + ], + INDEXES => [ + + # We do lookups by every item in the table simultaneously, but + # having an index with all three items would be the same size as + # the table. So instead we have an index on just the smallest item, + # to speed lookups. + login_failure_user_id_idx => ['user_id'], + ], + }, + + + # "tokens" stores the tokens users receive when a password or email + # change is requested. Tokens provide an extra measure of security + # for these changes. + tokens => { + FIELDS => [ + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + issuedate => {TYPE => 'DATETIME', NOTNULL => 1}, + token => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + tokentype => {TYPE => 'varchar(16)', NOTNULL => 1}, + eventdata => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [tokens_userid_idx => ['userid'],], + }, + + # GROUPS + # ------ + + groups => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(255)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, + userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + icon_url => {TYPE => 'TINYTEXT'}, ], + INDEXES => [groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + group_control_map => { + FIELDS => [ + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + entry => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + membercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + othercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + group_control_map_product_id_idx => + {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, + group_control_map_group_id_idx => ['group_id'], + ], + }, + + # "user_group_map" determines the groups that a user belongs to + # directly or due to regexp and which groups can be blessed by a user. + # + # grant_type: + # if GRANT_DIRECT - record was explicitly granted + # if GRANT_DERIVED - record was derived from expanding a group hierarchy + # if GRANT_REGEXP - record was created by evaluating a regexp + user_group_map => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GRANT_DIRECT}, + ], + INDEXES => [ + user_group_map_user_id_idx => + {FIELDS => [qw(user_id group_id grant_type isbless)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups are made a member of another + # group, given the ability to bless another group, or given + # visibility to another groups existence and membership + # grant_type: + # if GROUP_MEMBERSHIP - member groups are made members of grantor + # if GROUP_BLESS - member groups may grant membership in grantor + # if GROUP_VISIBLE - member groups may see grantor group + group_group_map => { + FIELDS => [ + member_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grantor_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GROUP_MEMBERSHIP}, + ], + INDEXES => [ + group_group_map_member_id_idx => + {FIELDS => [qw(member_id grantor_id grant_type)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a bug. + bug_group_map => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bug_group_map_bug_id_idx => {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, + bug_group_map_group_id_idx => ['group_id'], + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a named query somebody else shares. + namedquery_group_map => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedquery_group_map_namedquery_id_idx => + {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, + namedquery_group_map_group_id_idx => ['group_id'], + ], + }, + + category_group_map => { + FIELDS => [ + category_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + category_group_map_category_id_idx => + {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, + ], + }, + + + # PRODUCTS + # -------- + + classifications => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => + [classifications_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + products => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + classification_id => { + TYPE => 'INT2', + NOTNULL => 1, + DEFAULT => '1', + REFERENCES => {TABLE => 'classifications', COLUMN => 'id', DELETE => 'CASCADE'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 1}, + defaultmilestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + components => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + initialowner => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + initialqacontact => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + components_product_id_idx => + {FIELDS => [qw(product_id name)], TYPE => 'UNIQUE'}, + components_name_idx => ['name'], + ], + }, + + # CHARTS + # ------ + + series => { + FIELDS => [ + series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + creator => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + category => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + subcategory => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + frequency => {TYPE => 'INT2', NOTNULL => 1}, + query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + series_creator_idx => ['creator'], + series_category_idx => + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}, + ], + }, + + series_data => { + FIELDS => [ + series_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'series', COLUMN => 'series_id', DELETE => 'CASCADE'} + }, + series_date => {TYPE => 'DATETIME', NOTNULL => 1}, + series_value => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => [ + series_data_series_id_idx => + {FIELDS => [qw(series_id series_date)], TYPE => 'UNIQUE'}, + ], + }, + + series_categories => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => + [series_categories_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + # WHINE SYSTEM + # ------------ + + whine_queries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + query_name => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + title => {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [whine_queries_eventid_idx => ['eventid'],], + }, + + whine_schedules => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + run_day => {TYPE => 'varchar(32)'}, + run_time => {TYPE => 'varchar(32)'}, + run_next => {TYPE => 'DATETIME'}, + mailto => {TYPE => 'INT3', NOTNULL => 1}, + mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => [ + whine_schedules_run_next_idx => ['run_next'], + whine_schedules_eventid_idx => ['eventid'], + ], + }, + + whine_events => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + owner_userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + subject => {TYPE => 'varchar(128)'}, + body => {TYPE => 'MEDIUMTEXT'}, + mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + }, + + # QUIPS + # ----- + + quips => { + FIELDS => [ + quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + quip => {TYPE => 'varchar(512)', NOTNULL => 1}, + approved => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + }, + + # SETTINGS + # -------- + # setting - each global setting will have exactly one entry + # in this table. + # setting_value - stores the list of acceptable values for each + # setting, and a sort index that controls the order + # in which the values are displayed. + # profile_setting - If a user has chosen to use a value other than the + # global default for a given setting, it will be + # stored in this table. Note: even if a setting is + # later changed so is_enabled = false, the stored + # value will remain in case it is ever enabled again. + # + setting => { + FIELDS => [ + name => {TYPE => 'varchar(32)', NOTNULL => 1, PRIMARYKEY => 1}, + default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + subclass => {TYPE => 'varchar(32)'}, + ], + }, + + setting_value => { + FIELDS => [ + name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(32)', NOTNULL => 1}, + sortindex => {TYPE => 'INT2', NOTNULL => 1}, + ], + INDEXES => [ + setting_value_nv_unique_idx => {FIELDS => [qw(name value)], TYPE => 'UNIQUE'}, + setting_value_ns_unique_idx => + {FIELDS => [qw(name sortindex)], TYPE => 'UNIQUE'}, + ], + }, + + profile_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + setting_name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + ], + INDEXES => [ + profile_setting_value_unique_idx => + {FIELDS => [qw(user_id setting_name)], TYPE => 'UNIQUE'}, + ], + }, + + # BUGMAIL + # ------- + + mail_staging => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + message => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + # THESCHWARTZ TABLES + # ------------------ + # Note: In the standard TheSchwartz schema, most integers are unsigned, + # but we didn't implement unsigned ints for Bugzilla schemas, so we + # just create signed ints, which should be fine. + + ts_funcmap => { + FIELDS => [ + funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, + ], + INDEXES => + [ts_funcmap_funcname_idx => {FIELDS => ['funcname'], TYPE => 'UNIQUE'},], + }, + + ts_job => { + FIELDS => [ + + # In a standard TheSchwartz schema, this is a BIGINT, but we + # don't have those and I didn't want to add them just for this. + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1}, + + # In standard TheSchwartz, this is a MEDIUMBLOB. + arg => {TYPE => 'LONGBLOB'}, + uniqkey => {TYPE => 'varchar(255)'}, + insert_time => {TYPE => 'INT4'}, + run_after => {TYPE => 'INT4', NOTNULL => 1}, + grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, + priority => {TYPE => 'INT2'}, + coalesce => {TYPE => 'varchar(255)'}, + ], + INDEXES => [ + ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], TYPE => 'UNIQUE'}, + + # In a standard TheSchewartz schema, these both go in the other + # direction, but there's no reason to have three indexes that + # all start with the same column, and our naming scheme doesn't + # allow it anyhow. + ts_job_run_after_idx => [qw(run_after funcid)], + ts_job_coalesce_idx => [qw(coalesce funcid)], + ], + }, + + ts_note => { + FIELDS => [ + + # This is a BIGINT in standard TheSchwartz schemas. + jobid => {TYPE => 'INT4', NOTNULL => 1}, + notekey => {TYPE => 'varchar(255)'}, + value => {TYPE => 'LONGBLOB'}, + ], + INDEXES => + [ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], TYPE => 'UNIQUE'},], + }, + + ts_error => { + FIELDS => [ + error_time => {TYPE => 'INT4', NOTNULL => 1}, + jobid => {TYPE => 'INT4', NOTNULL => 1}, + message => {TYPE => 'varchar(255)', NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + ts_error_funcid_idx => [qw(funcid error_time)], + ts_error_error_time_idx => ['error_time'], + ts_error_jobid_idx => ['jobid'], + ], + }, + + ts_exitstatus => { + FIELDS => [ + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + status => {TYPE => 'INT2'}, + completion_time => {TYPE => 'INT4'}, + delete_after => {TYPE => 'INT4'}, + ], + INDEXES => [ + ts_exitstatus_funcid_idx => ['funcid'], + ts_exitstatus_delete_after_idx => ['delete_after'], + ], + }, + + # SCHEMA STORAGE + # -------------- + + bz_schema => { + FIELDS => [ + schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, + version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, + ], + }, + + bug_user_last_visit => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [ + bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], TYPE => 'UNIQUE'}, + bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], + ], + }, + + user_api_keys => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, + description => {TYPE => 'VARCHAR(255)'}, + revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + last_used => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id_idx => ['user_id'], + ], + }, +}; + +# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables +use constant MULTI_SELECT_VALUE_TABLE => { + FIELDS => [ + bug_id => {TYPE => 'INT3', NOTNULL => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},], }; #-------------------------------------------------------------------------- @@ -1821,27 +1770,28 @@ sub new { =cut - my $this = shift; - my $class = ref($this) || $this; - my $driver = shift; + my $this = shift; + my $class = ref($this) || $this; + my $driver = shift; - if ($driver) { - (my $subclass = $driver) =~ s/^(\S)/\U$1/; - $class .= '::' . $subclass; - eval "require $class;"; - die "The $class class could not be found ($subclass " . - "not supported?): $@" if ($@); - } - die "$class is an abstract base class. Instantiate a subclass instead." - if ($class eq __PACKAGE__); + if ($driver) { + (my $subclass = $driver) =~ s/^(\S)/\U$1/; + $class .= '::' . $subclass; + eval "require $class;"; + die "The $class class could not be found ($subclass " . "not supported?): $@" + if ($@); + } + die "$class is an abstract base class. Instantiate a subclass instead." + if ($class eq __PACKAGE__); + + my $self = {}; + bless $self, $class; + $self = $self->_initialize(@_); - my $self = {}; - bless $self, $class; - $self = $self->_initialize(@_); + return ($self); - return($self); +} #eosub--new -} #eosub--new #-------------------------------------------------------------------------- sub _initialize { @@ -1864,33 +1814,34 @@ sub _initialize { =cut - my $self = shift; - my $abstract_schema = shift; + my $self = shift; + my $abstract_schema = shift; - if (!$abstract_schema) { - # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. - # So, we dclone it to prevent anything from mucking with the constant. - $abstract_schema = dclone(ABSTRACT_SCHEMA); + if (!$abstract_schema) { - # Let extensions add tables, but make sure they can't modify existing - # tables. If we don't lock/unlock keys, lock_value complains. - lock_keys(%$abstract_schema); - foreach my $table (keys %{ABSTRACT_SCHEMA()}) { - lock_value(%$abstract_schema, $table) - if exists $abstract_schema->{$table}; - } - unlock_keys(%$abstract_schema); - Bugzilla::Hook::process('db_schema_abstract_schema', - { schema => $abstract_schema }); - unlock_hash(%$abstract_schema); + # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. + # So, we dclone it to prevent anything from mucking with the constant. + $abstract_schema = dclone(ABSTRACT_SCHEMA); + + # Let extensions add tables, but make sure they can't modify existing + # tables. If we don't lock/unlock keys, lock_value complains. + lock_keys(%$abstract_schema); + foreach my $table (keys %{ABSTRACT_SCHEMA()}) { + lock_value(%$abstract_schema, $table) if exists $abstract_schema->{$table}; } + unlock_keys(%$abstract_schema); + Bugzilla::Hook::process('db_schema_abstract_schema', + {schema => $abstract_schema}); + unlock_hash(%$abstract_schema); + } + + $self->{schema} = dclone($abstract_schema); + $self->{abstract_schema} = $abstract_schema; - $self->{schema} = dclone($abstract_schema); - $self->{abstract_schema} = $abstract_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------------- sub _adjust_schema { @@ -1906,36 +1857,41 @@ sub _adjust_schema { =cut - my $self = shift; - - # The _initialize method has already set up the db_specific hash with - # the information on how to implement the abstract data types for the - # instantiated DBMS-specific subclass. - my $db_specific = $self->{db_specific}; - - # Loop over each table in the abstract database schema. - foreach my $table (keys %{ $self->{schema} }) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - # Loop over the field definitions in each table. - foreach my $field_def (values %fields) { - # If the field type is an abstract data type defined in the - # $db_specific hash, replace it with the DBMS-specific data type - # that implements it. - if (exists($db_specific->{$field_def->{TYPE}})) { - $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; - } - # Replace abstract default values (such as 'TRUE' and 'FALSE') - # with their database-specific implementations. - if (exists($field_def->{DEFAULT}) - && exists($db_specific->{$field_def->{DEFAULT}})) { - $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; - } - } + my $self = shift; + + # The _initialize method has already set up the db_specific hash with + # the information on how to implement the abstract data types for the + # instantiated DBMS-specific subclass. + my $db_specific = $self->{db_specific}; + + # Loop over each table in the abstract database schema. + foreach my $table (keys %{$self->{schema}}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + + # Loop over the field definitions in each table. + foreach my $field_def (values %fields) { + + # If the field type is an abstract data type defined in the + # $db_specific hash, replace it with the DBMS-specific data type + # that implements it. + if (exists($db_specific->{$field_def->{TYPE}})) { + $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; + } + + # Replace abstract default values (such as 'TRUE' and 'FALSE') + # with their database-specific implementations. + if ( exists($field_def->{DEFAULT}) + && exists($db_specific->{$field_def->{DEFAULT}})) + { + $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; + } } + } + + return $self; - return $self; +} #eosub--_adjust_schema -} #eosub--_adjust_schema #-------------------------------------------------------------------------- sub get_type_ddl { @@ -1969,30 +1925,34 @@ C<ALTER TABLE> SQL statement =cut - my $self = shift; - my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ }; - my $type = $finfo->{TYPE}; - confess "A valid TYPE was not specified for this column (got " - . Dumper($finfo) . ")" unless ($type); - - my $default = $finfo->{DEFAULT}; - # Replace any abstract default value (such as 'TRUE' or 'FALSE') - # with its database-specific implementation. - if ( defined $default && exists($self->{db_specific}->{$default}) ) { - $default = $self->{db_specific}->{$default}; - } + my $self = shift; + my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : {@_}; + my $type = $finfo->{TYPE}; + confess "A valid TYPE was not specified for this column (got " + . Dumper($finfo) . ")" + unless ($type); + + my $default = $finfo->{DEFAULT}; + + # Replace any abstract default value (such as 'TRUE' or 'FALSE') + # with its database-specific implementation. + if (defined $default && exists($self->{db_specific}->{$default})) { + $default = $self->{db_specific}->{$default}; + } + + my $type_ddl = $self->convert_type($type); + + # DEFAULT attribute must appear before any column constraints + # (e.g., NOT NULL), for Oracle + $type_ddl .= " DEFAULT $default" if (defined($default)); - my $type_ddl = $self->convert_type($type); - # DEFAULT attribute must appear before any column constraints - # (e.g., NOT NULL), for Oracle - $type_ddl .= " DEFAULT $default" if (defined($default)); - # PRIMARY KEY must appear before NOT NULL for SQLite. - $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); - $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); + # PRIMARY KEY must appear before NOT NULL for SQLite. + $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); + $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); - return($type_ddl); + return ($type_ddl); -} #eosub--get_type_ddl +} #eosub--get_type_ddl sub get_fk_ddl { @@ -2026,78 +1986,80 @@ is undefined. =cut - my ($self, $table, $column, $references) = @_; - return "" if !$references; + my ($self, $table, $column, $references) = @_; + return "" if !$references; - my $update = $references->{UPDATE} || 'CASCADE'; - my $delete = $references->{DELETE} || 'RESTRICT'; - my $to_table = $references->{TABLE} || confess "No table in reference"; - my $to_column = $references->{COLUMN} || confess "No column in reference"; - my $fk_name = $self->_get_fk_name($table, $column, $references); + my $update = $references->{UPDATE} || 'CASCADE'; + my $delete = $references->{DELETE} || 'RESTRICT'; + my $to_table = $references->{TABLE} || confess "No table in reference"; + my $to_column = $references->{COLUMN} || confess "No column in reference"; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" - . " REFERENCES $to_table($to_column)\n" - . " ON UPDATE $update ON DELETE $delete"; + return + "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" + . " REFERENCES $to_table($to_column)\n" + . " ON UPDATE $update ON DELETE $delete"; } # Generates a name for a Foreign Key. It's separate from get_fk_ddl # so that certain databases can override it (for shorter identifiers or # other reasons). sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $name = "fk_${table}_${column}_${to_table}_${to_column}"; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $name = "fk_${table}_${column}_${to_table}_${to_column}"; - if (length($name) > $self->MAX_IDENTIFIER_LEN) { - $name = 'fk_' . $self->_hash_identifier($name); - } + if (length($name) > $self->MAX_IDENTIFIER_LEN) { + $name = 'fk_' . $self->_hash_identifier($name); + } - return $name; + return $name; } sub _hash_identifier { - my ($invocant, $value) = @_; - # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something - # longer in the future. - return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); + my ($invocant, $value) = @_; + + # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something + # longer in the future. + return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); } sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - - my @add = $self->_column_fks_to_ddl($table, $column_fks); - - my @sql; - if ($self->MULTIPLE_FKS_IN_ALTER) { - my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); - push(@sql, $alter); + my ($self, $table, $column_fks) = @_; + + my @add = $self->_column_fks_to_ddl($table, $column_fks); + + my @sql; + if ($self->MULTIPLE_FKS_IN_ALTER) { + my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); + push(@sql, $alter); + } + else { + foreach my $fk_string (@add) { + push(@sql, "ALTER TABLE $table ADD $fk_string"); } - else { - foreach my $fk_string (@add) { - push(@sql, "ALTER TABLE $table ADD $fk_string"); - } - } - return @sql; + } + return @sql; } sub _column_fks_to_ddl { - my ($self, $table, $column_fks) = @_; - my @ddl; - foreach my $column (keys %$column_fks) { - my $def = $column_fks->{$column}; - my $fk_string = $self->get_fk_ddl($table, $column, $def); - push(@ddl, $fk_string); - } - return @ddl; + my ($self, $table, $column_fks) = @_; + my @ddl; + foreach my $column (keys %$column_fks) { + my $def = $column_fks->{$column}; + my $fk_string = $self->get_fk_ddl($table, $column, $def); + push(@ddl, $fk_string); + } + return @ddl; } -sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); +sub get_drop_fk_sql { + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); + return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); } sub convert_type { @@ -2108,8 +2070,8 @@ Converts a TYPE from the L</ABSTRACT_SCHEMA> format into the real SQL type. =cut - my ($self, $type) = @_; - return $self->{db_specific}->{$type} || $type; + my ($self, $type) = @_; + return $self->{db_specific}->{$type} || $type; } sub get_column { @@ -2126,16 +2088,16 @@ sub get_column { =cut - my($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if (exists $self->{schema}->{$table}) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; -} #eosub--get_column + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if (exists $self->{schema}->{$table}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; +} #eosub--get_column sub get_table_list { @@ -2150,8 +2112,8 @@ sub get_table_list { =cut - my $self = shift; - return sort keys %{$self->{schema}}; + my $self = shift; + return sort keys %{$self->{schema}}; } sub get_table_columns { @@ -2165,34 +2127,33 @@ sub get_table_columns { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless (ref($thash)); + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless (ref($thash)); - my @columns = (); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - push(@columns, shift(@fields)); - shift(@fields); - } + my @columns = (); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + push(@columns, shift(@fields)); + shift(@fields); + } - return @columns; + return @columns; -} #eosub--get_table_columns +} #eosub--get_table_columns sub get_table_indexes_abstract { - my ($self, $table) = @_; - my $table_def = $self->get_table_abstract($table); - my %indexes = @{$table_def->{INDEXES} || []}; - return \%indexes; + my ($self, $table) = @_; + my $table_def = $self->get_table_abstract($table); + my %indexes = @{$table_def->{INDEXES} || []}; + return \%indexes; } sub get_create_database_sql { - my ($self, $name) = @_; - return ("CREATE DATABASE $name"); + my ($self, $name) = @_; + return ("CREATE DATABASE $name"); } sub get_table_ddl { @@ -2209,30 +2170,29 @@ sub get_table_ddl { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - die "Table $table does not exist in the database schema." - unless (ref($self->{schema}{$table})); + die "Table $table does not exist in the database schema." + unless (ref($self->{schema}{$table})); - my $create_table = $self->_get_create_table_ddl($table); - push(@ddl, $create_table) if $create_table; + my $create_table = $self->_get_create_table_ddl($table); + push(@ddl, $create_table) if $create_table; - my @indexes = @{ $self->{schema}{$table}{INDEXES} || [] }; - while (@indexes) { - my $index_name = shift(@indexes); - my $index_info = shift(@indexes); - my $index_sql = $self->get_add_index_ddl($table, $index_name, - $index_info); - push(@ddl, $index_sql) if $index_sql; - } + my @indexes = @{$self->{schema}{$table}{INDEXES} || []}; + while (@indexes) { + my $index_name = shift(@indexes); + my $index_info = shift(@indexes); + my $index_sql = $self->get_add_index_ddl($table, $index_name, $index_info); + push(@ddl, $index_sql) if $index_sql; + } - push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} }) - if (ref($self->{schema}{$table}{DB_EXTRAS})); + push(@ddl, @{$self->{schema}{$table}{DB_EXTRAS}}) + if (ref($self->{schema}{$table}{DB_EXTRAS})); - return @ddl; + return @ddl; -} #eosub--get_table_ddl +} #eosub--get_table_ddl sub _get_create_table_ddl { @@ -2245,30 +2205,29 @@ sub _get_create_table_ddl { =cut - my($self, $table) = @_; - - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless ref $thash; - - my (@col_lines, @fk_lines); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - my $field = shift(@fields); - my $finfo = shift(@fields); - push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); - if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { - my $fk = $finfo->{REFERENCES}; - my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); - push(@fk_lines, $fk_ddl); - } + my ($self, $table) = @_; + + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless ref $thash; + + my (@col_lines, @fk_lines); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + my $field = shift(@fields); + my $finfo = shift(@fields); + push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); + if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { + my $fk = $finfo->{REFERENCES}; + my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); + push(@fk_lines, $fk_ddl); } - - my $sql = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) - . "\n)"; - return $sql + } -} + my $sql + = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) . "\n)"; + return $sql; + +} sub _get_create_index_ddl { @@ -2284,16 +2243,17 @@ sub _get_create_index_ddl { =cut - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + + my $sql = "CREATE "; + $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); + $sql + .= "INDEX $index_name ON $table_name \(" . join(", ", @$index_fields) . "\)"; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); - $sql .= "INDEX $index_name ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + return ($sql); - return($sql); +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------------- sub get_add_column_ddl { @@ -2312,22 +2272,25 @@ sub get_add_column_ddl { =cut - my ($self, $table, $column, $definition, $init_value) = @_; - my @statements; - push(@statements, "ALTER TABLE $table ". $self->ADD_COLUMN ." $column " . - $self->get_type_ddl($definition)); - - # XXX - Note that although this works for MySQL, most databases will fail - # before this point, if we haven't set a default. - (push(@statements, "UPDATE $table SET $column = $init_value")) - if defined $init_value; - - if (defined $definition->{REFERENCES}) { - push(@statements, $self->get_add_fks_sql($table, { $column => - $definition->{REFERENCES} })); - } - - return (@statements); + my ($self, $table, $column, $definition, $init_value) = @_; + my @statements; + push(@statements, + "ALTER TABLE $table " + . $self->ADD_COLUMN + . " $column " + . $self->get_type_ddl($definition)); + + # XXX - Note that although this works for MySQL, most databases will fail + # before this point, if we haven't set a default. + (push(@statements, "UPDATE $table SET $column = $init_value")) + if defined $init_value; + + if (defined $definition->{REFERENCES}) { + push(@statements, + $self->get_add_fks_sql($table, {$column => $definition->{REFERENCES}})); + } + + return (@statements); } sub get_add_index_ddl { @@ -2348,20 +2311,21 @@ sub get_add_index_ddl { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my ($index_fields, $index_type); - # Index defs can be arrays or hashes - if (ref($definition) eq 'HASH') { - $index_fields = $definition->{FIELDS}; - $index_type = $definition->{TYPE}; - } else { - $index_fields = $definition; - $index_type = ''; - } - - return $self->_get_create_index_ddl($table, $name, $index_fields, - $index_type); + my ($index_fields, $index_type); + + # Index defs can be arrays or hashes + if (ref($definition) eq 'HASH') { + $index_fields = $definition->{FIELDS}; + $index_type = $definition->{TYPE}; + } + else { + $index_fields = $definition; + $index_type = ''; + } + + return $self->_get_create_index_ddl($table, $name, $index_fields, $index_type); } sub get_alter_column_ddl { @@ -2384,85 +2348,88 @@ sub get_alter_column_ddl { =cut - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP DEFAULT"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column " - . " SET DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - push(@statements, $self->_set_nulls_sql(@_)); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " SET NOT NULL"); - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP NOT NULL"); - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); - } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP DEFAULT"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, + "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + push(@statements, $self->_set_nulls_sql(@_)); + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL"); + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL"); + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } # Helps handle any fields that were NULL before, if we have a default, # when doing an ALTER COLUMN. sub _set_nulls_sql { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $default = $new_def->{DEFAULT}; - # If we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $default = $set_nulls_to if defined $set_nulls_to; - if (defined $default) { - my $specific = $self->{db_specific}; - $default = $specific->{$default} if exists $specific->{$default}; - } - my @sql; - if (defined $default) { - push(@sql, "UPDATE $table SET $column = $default" - . " WHERE $column IS NULL"); - } - return @sql; + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $default = $new_def->{DEFAULT}; + + # If we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $default = $set_nulls_to if defined $set_nulls_to; + if (defined $default) { + my $specific = $self->{db_specific}; + $default = $specific->{$default} if exists $specific->{$default}; + } + my @sql; + if (defined $default) { + push(@sql, "UPDATE $table SET $column = $default" . " WHERE $column IS NULL"); + } + return @sql; } sub get_drop_index_ddl { @@ -2476,11 +2443,11 @@ sub get_drop_index_ddl { =cut - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - # Although ANSI SQL-92 doesn't specify a method of dropping an index, - # many DBs support this syntax. - return ("DROP INDEX $name"); + # Although ANSI SQL-92 doesn't specify a method of dropping an index, + # many DBs support this syntax. + return ("DROP INDEX $name"); } sub get_drop_column_ddl { @@ -2494,8 +2461,8 @@ sub get_drop_column_ddl { =cut - my ($self, $table, $column) = @_; - return ("ALTER TABLE $table DROP COLUMN $column"); + my ($self, $table, $column) = @_; + return ("ALTER TABLE $table DROP COLUMN $column"); } =item C<get_drop_table_ddl($table)> @@ -2507,8 +2474,8 @@ sub get_drop_column_ddl { =cut sub get_drop_table_ddl { - my ($self, $table) = @_; - return ("DROP TABLE $table"); + my ($self, $table) = @_; + return ("DROP TABLE $table"); } sub get_rename_column_ddl { @@ -2526,8 +2493,8 @@ sub get_rename_column_ddl { =cut - die "ANSI SQL has no way to rename a column, and your database driver\n" - . " has not implemented a method."; + die "ANSI SQL has no way to rename a column, and your database driver\n" + . " has not implemented a method."; } @@ -2557,8 +2524,8 @@ Gets SQL to rename a table in the database. =cut - my ($self, $old_name, $new_name) = @_; - return ("ALTER TABLE $old_name RENAME TO $new_name"); + my ($self, $old_name, $new_name) = @_; + return ("ALTER TABLE $old_name RENAME TO $new_name"); } =item C<delete_table($name)> @@ -2571,13 +2538,13 @@ Gets SQL to rename a table in the database. =cut sub delete_table { - my ($self, $name) = @_; + my ($self, $name) = @_; - die "Attempted to delete nonexistent table '$name'." unless - $self->get_table_abstract($name); + die "Attempted to delete nonexistent table '$name'." + unless $self->get_table_abstract($name); - delete $self->{abstract_schema}->{$name}; - delete $self->{schema}->{$name}; + delete $self->{abstract_schema}->{$name}; + delete $self->{schema}->{$name}; } sub get_column_abstract { @@ -2594,15 +2561,15 @@ sub get_column_abstract { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if ($self->get_table_abstract($table)) { - my %fields = (@{ $self->{abstract_schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if ($self->get_table_abstract($table)) { + my %fields = (@{$self->{abstract_schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; } =item C<get_indexes_on_column_abstract($table, $column)> @@ -2620,29 +2587,31 @@ sub get_column_abstract { =cut sub get_indexes_on_column_abstract { - my ($self, $table, $column) = @_; - my %ret_hash; - - my $table_def = $self->get_table_abstract($table); - if ($table_def && exists $table_def->{INDEXES}) { - my %indexes = (@{ $table_def->{INDEXES} }); - foreach my $index_name (keys %indexes) { - my $col_list; - # Get the column list, depending on whether the index - # is in hashref or arrayref format. - if (ref($indexes{$index_name}) eq 'HASH') { - $col_list = $indexes{$index_name}->{FIELDS}; - } else { - $col_list = $indexes{$index_name}; - } - - if(grep($_ eq $column, @$col_list)) { - $ret_hash{$index_name} = dclone($indexes{$index_name}); - } - } + my ($self, $table, $column) = @_; + my %ret_hash; + + my $table_def = $self->get_table_abstract($table); + if ($table_def && exists $table_def->{INDEXES}) { + my %indexes = (@{$table_def->{INDEXES}}); + foreach my $index_name (keys %indexes) { + my $col_list; + + # Get the column list, depending on whether the index + # is in hashref or arrayref format. + if (ref($indexes{$index_name}) eq 'HASH') { + $col_list = $indexes{$index_name}->{FIELDS}; + } + else { + $col_list = $indexes{$index_name}; + } + + if (grep($_ eq $column, @$col_list)) { + $ret_hash{$index_name} = dclone($indexes{$index_name}); + } } + } - return %ret_hash; + return %ret_hash; } sub get_index_abstract { @@ -2658,16 +2627,16 @@ sub get_index_abstract { =cut - my ($self, $table, $index) = @_; + my ($self, $table, $index) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - my $index_table = $self->get_table_abstract($table); - if ($index_table && exists $index_table->{INDEXES}) { - my %indexes = (@{ $index_table->{INDEXES} }); - return $indexes{$index}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + my $index_table = $self->get_table_abstract($table); + if ($index_table && exists $index_table->{INDEXES}) { + my %indexes = (@{$index_table->{INDEXES}}); + return $indexes{$index}; + } + return undef; } =item C<get_table_abstract($table)> @@ -2681,8 +2650,8 @@ sub get_index_abstract { =cut sub get_table_abstract { - my ($self, $table) = @_; - return $self->{abstract_schema}->{$table}; + my ($self, $table) = @_; + return $self->{abstract_schema}->{$table}; } =item C<add_table($name, \%definition)> @@ -2698,22 +2667,20 @@ sub get_table_abstract { =cut sub add_table { - my ($self, $name, $definition) = @_; - (die "Table already exists: $name") - if exists $self->{abstract_schema}->{$name}; - if ($definition) { - $self->{abstract_schema}->{$name} = dclone($definition); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); - } - else { - $self->{abstract_schema}->{$name} = {FIELDS => []}; - $self->{schema}->{$name} = {FIELDS => []}; - } + my ($self, $name, $definition) = @_; + (die "Table already exists: $name") if exists $self->{abstract_schema}->{$name}; + if ($definition) { + $self->{abstract_schema}->{$name} = dclone($definition); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); + } + else { + $self->{abstract_schema}->{$name} = {FIELDS => []}; + $self->{schema}->{$name} = {FIELDS => []}; + } } - sub rename_table { =item C<rename_table> @@ -2723,10 +2690,10 @@ Renames a table from C<$old_name> to C<$new_name> in this Schema object. =cut - my ($self, $old_name, $new_name) = @_; - my $table = $self->get_table_abstract($old_name); - $self->delete_table($old_name); - $self->add_table($new_name, $table); + my ($self, $old_name, $new_name) = @_; + my $table = $self->get_table_abstract($old_name); + $self->delete_table($old_name); + $self->add_table($new_name, $table); } sub delete_column { @@ -2741,17 +2708,18 @@ sub delete_column { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; - my $name_position = firstidx { $_ eq $column } @$abstract_fields; - die "Attempted to delete nonexistent column ${table}.${column}" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$abstract_fields, $name_position, 2); + my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; + my $name_position = firstidx { $_ eq $column } @$abstract_fields; + die "Attempted to delete nonexistent column ${table}.${column}" + if $name_position == -1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # Delete the key/value pair from the array. + splice(@$abstract_fields, $name_position, 2); + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub rename_column { @@ -2767,11 +2735,11 @@ sub rename_column { =cut - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_column_abstract($table, $old_name); - die "Renaming a column that doesn't exist" if !$def; - $self->delete_column($table, $old_name); - $self->set_column($table, $new_name, $def); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_column_abstract($table, $old_name); + die "Renaming a column that doesn't exist" if !$def; + $self->delete_column($table, $old_name); + $self->set_column($table, $new_name, $def); } sub set_column { @@ -2792,10 +2760,10 @@ sub set_column { =cut - my ($self, $table, $column, $new_def) = @_; + my ($self, $table, $column, $new_def) = @_; - my $fields = $self->{abstract_schema}{$table}{FIELDS}; - $self->_set_object($table, $column, $new_def, $fields); + my $fields = $self->{abstract_schema}{$table}{FIELDS}; + $self->_set_object($table, $column, $new_def, $fields); } =item C<set_fk($table, $column \%fk_def)> @@ -2805,19 +2773,20 @@ Sets the C<REFERENCES> item on the specified column. =cut sub set_fk { - my ($self, $table, $column, $fk_def) = @_; - # Don't want to modify the source def before we explicitly set it below. - # This is just us being extra-cautious. - my $column_def = dclone($self->get_column_abstract($table, $column)); - die "Tried to set an fk on $table.$column, but that column doesn't exist" - if !$column_def; - if ($fk_def) { - $column_def->{REFERENCES} = $fk_def; - } - else { - delete $column_def->{REFERENCES}; - } - $self->set_column($table, $column, $column_def); + my ($self, $table, $column, $fk_def) = @_; + + # Don't want to modify the source def before we explicitly set it below. + # This is just us being extra-cautious. + my $column_def = dclone($self->get_column_abstract($table, $column)); + die "Tried to set an fk on $table.$column, but that column doesn't exist" + if !$column_def; + if ($fk_def) { + $column_def->{REFERENCES} = $fk_def; + } + else { + delete $column_def->{REFERENCES}; + } + $self->set_column($table, $column, $column_def); } sub set_index { @@ -2838,36 +2807,39 @@ sub set_index { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - if ( exists $self->{abstract_schema}{$table} - && !exists $self->{abstract_schema}{$table}{INDEXES} ) { - $self->{abstract_schema}{$table}{INDEXES} = []; - } + if (exists $self->{abstract_schema}{$table} + && !exists $self->{abstract_schema}{$table}{INDEXES}) + { + $self->{abstract_schema}{$table}{INDEXES} = []; + } - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - $self->_set_object($table, $name, $definition, $indexes); + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + $self->_set_object($table, $name, $definition, $indexes); } # A private helper for set_index and set_column. # This does the actual "work" of those two functions. # $array_to_change is an arrayref. sub _set_object { - my ($self, $table, $name, $definition, $array_to_change) = @_; + my ($self, $table, $name, $definition, $array_to_change) = @_; - my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - # If the object doesn't exist, then add it. - if (!$obj_position) { - push(@$array_to_change, $name); - push(@$array_to_change, $definition); - } - # We're modifying an existing object in the Schema. - else { - splice(@$array_to_change, $obj_position, 1, $definition); - } + my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # If the object doesn't exist, then add it. + if (!$obj_position) { + push(@$array_to_change, $name); + push(@$array_to_change, $definition); + } + + # We're modifying an existing object in the Schema. + else { + splice(@$array_to_change, $obj_position, 1, $definition); + } + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } =item C<delete_index($table, $name)> @@ -2885,16 +2857,17 @@ sub _set_object { =cut sub delete_index { - my ($self, $table, $name) = @_; - - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - my $name_position = firstidx { $_ eq $name } @$indexes; - die "Attempted to delete nonexistent index $name on the $table table" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$indexes, $name_position, 2); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + my ($self, $table, $name) = @_; + + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + my $name_position = firstidx { $_ eq $name } @$indexes; + die "Attempted to delete nonexistent index $name on the $table table" + if $name_position == -1; + + # Delete the key/value pair from the array. + splice(@$indexes, $name_position, 2); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub columns_equal { @@ -2912,24 +2885,24 @@ sub columns_equal { =cut - my $self = shift; - my $col_one = dclone(shift); - my $col_two = dclone(shift); + my $self = shift; + my $col_one = dclone(shift); + my $col_two = dclone(shift); - $col_one->{TYPE} = uc($col_one->{TYPE}); - $col_two->{TYPE} = uc($col_two->{TYPE}); + $col_one->{TYPE} = uc($col_one->{TYPE}); + $col_two->{TYPE} = uc($col_two->{TYPE}); - # We don't care about foreign keys when comparing column definitions. - delete $col_one->{REFERENCES}; - delete $col_two->{REFERENCES}; + # We don't care about foreign keys when comparing column definitions. + delete $col_one->{REFERENCES}; + delete $col_two->{REFERENCES}; - my @col_one_array = %$col_one; - my @col_two_array = %$col_two; + my @col_one_array = %$col_one; + my @col_two_array = %$col_two; - my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); + my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); - # If there are no differences between the arrays, then they are equal. - return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; + # If there are no differences between the arrays, then they are equal. + return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; } @@ -2953,18 +2926,18 @@ sub columns_equal { =cut sub serialize_abstract { - my ($self) = @_; - - # Make it ok to eval - local $Data::Dumper::Purity = 1; - - # Avoid cross-refs - local $Data::Dumper::Deepcopy = 1; - - # Always sort keys to allow textual compare - local $Data::Dumper::Sortkeys = 1; - - return Dumper($self->{abstract_schema}); + my ($self) = @_; + + # Make it ok to eval + local $Data::Dumper::Purity = 1; + + # Avoid cross-refs + local $Data::Dumper::Deepcopy = 1; + + # Always sort keys to allow textual compare + local $Data::Dumper::Sortkeys = 1; + + return Dumper($self->{abstract_schema}); } =item C<deserialize_abstract($serialized, $version)> @@ -2983,36 +2956,34 @@ sub serialize_abstract { =cut sub deserialize_abstract { - my ($class, $serialized, $version) = @_; - - my $thawed_hash; - if ($version < 2) { - $thawed_hash = thaw($serialized); - } - else { - my $cpt = new Safe; - $cpt->reval($serialized) || - die "Unable to restore cached schema: " . $@; - $thawed_hash = ${$cpt->varglob('VAR1')}; - } - - # Version 2 didn't have the "created" key for REFERENCES items. - if ($version < 3) { - my $standard = $class->new()->{abstract_schema}; - foreach my $table_name (keys %$thawed_hash) { - my %standard_fields = - @{ $standard->{$table_name}->{FIELDS} || [] }; - my $table = $thawed_hash->{$table_name}; - my %fields = @{ $table->{FIELDS} || [] }; - while (my ($field, $def) = each %fields) { - if (exists $def->{REFERENCES}) { - $def->{REFERENCES}->{created} = 1; - } - } + my ($class, $serialized, $version) = @_; + + my $thawed_hash; + if ($version < 2) { + $thawed_hash = thaw($serialized); + } + else { + my $cpt = new Safe; + $cpt->reval($serialized) || die "Unable to restore cached schema: " . $@; + $thawed_hash = ${$cpt->varglob('VAR1')}; + } + + # Version 2 didn't have the "created" key for REFERENCES items. + if ($version < 3) { + my $standard = $class->new()->{abstract_schema}; + foreach my $table_name (keys %$thawed_hash) { + my %standard_fields = @{$standard->{$table_name}->{FIELDS} || []}; + my $table = $thawed_hash->{$table_name}; + my %fields = @{$table->{FIELDS} || []}; + while (my ($field, $def) = each %fields) { + if (exists $def->{REFERENCES}) { + $def->{REFERENCES}->{created} = 1; } + } } + } - return $class->new(undef, $thawed_hash); + return $class->new(undef, $thawed_hash); } ##################################################################### @@ -3040,8 +3011,8 @@ object. =cut sub get_empty_schema { - my ($class) = @_; - return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); + my ($class) = @_; + return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); } 1; diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm index 7ff8ade9f..4c0d43523 100644 --- a/Bugzilla/DB/Schema/Mysql.pm +++ b/Bugzilla/DB/Schema/Mysql.pm @@ -21,7 +21,7 @@ use Bugzilla::Error; use parent qw(Bugzilla::DB::Schema); -# This is for column_info_to_column, to know when a tinyint is a +# This is for column_info_to_column, to know when a tinyint is a # boolean and when it's really a tinyint. This only has to be accurate # up to and through 2.19.3, because that's the only time we need # column_info_to_column. @@ -30,232 +30,260 @@ use parent qw(Bugzilla::DB::Schema); # that should be interpreted as a BOOLEAN instead of as an INT1 when # reading in the Schema from the disk. The values are discarded; I just # used "1" for simplicity. -# +# # THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T # UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS. use constant BOOLEAN_MAP => { - bugs => {everconfirmed => 1, reporter_accessible => 1, - cclist_accessible => 1, qacontact_accessible => 1, - assignee_accessible => 1}, - longdescs => {isprivate => 1, already_wrapped => 1}, - attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, - flags => {is_active => 1}, - flagtypes => {is_active => 1, is_requestable => 1, - is_requesteeble => 1, is_multiplicable => 1}, - fielddefs => {mailhead => 1, obsolete => 1}, - bug_status => {isactive => 1}, - resolution => {isactive => 1}, - bug_severity => {isactive => 1}, - priority => {isactive => 1}, - rep_platform => {isactive => 1}, - op_sys => {isactive => 1}, - profiles => {mybugslink => 1, newemailtech => 1}, - namedqueries => {linkinfooter => 1, watchfordiffs => 1}, - groups => {isbuggroup => 1, isactive => 1}, - group_control_map => {entry => 1, membercontrol => 1, othercontrol => 1, - canedit => 1}, - group_group_map => {isbless => 1}, - user_group_map => {isbless => 1, isderived => 1}, - products => {disallownew => 1}, - series => {public => 1}, - whine_queries => {onemailperbug => 1}, - quips => {approved => 1}, - setting => {is_enabled => 1} + bugs => { + everconfirmed => 1, + reporter_accessible => 1, + cclist_accessible => 1, + qacontact_accessible => 1, + assignee_accessible => 1 + }, + longdescs => {isprivate => 1, already_wrapped => 1}, + attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, + flags => {is_active => 1}, + flagtypes => { + is_active => 1, + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 1 + }, + fielddefs => {mailhead => 1, obsolete => 1}, + bug_status => {isactive => 1}, + resolution => {isactive => 1}, + bug_severity => {isactive => 1}, + priority => {isactive => 1}, + rep_platform => {isactive => 1}, + op_sys => {isactive => 1}, + profiles => {mybugslink => 1, newemailtech => 1}, + namedqueries => {linkinfooter => 1, watchfordiffs => 1}, + groups => {isbuggroup => 1, isactive => 1}, + group_control_map => + {entry => 1, membercontrol => 1, othercontrol => 1, canedit => 1}, + group_group_map => {isbless => 1}, + user_group_map => {isbless => 1, isderived => 1}, + products => {disallownew => 1}, + series => {public => 1}, + whine_queries => {onemailperbug => 1}, + quips => {approved => 1}, + setting => {is_enabled => 1} }; # Maps the db_specific hash backwards, for use in column_info_to_column. use constant REVERSE_MAPPING => { - # Boolean and the SERIAL fields are handled in column_info_to_column, - # and so don't have an entry here. - TINYINT => 'INT1', - SMALLINT => 'INT2', - MEDIUMINT => 'INT3', - INTEGER => 'INT4', - - # All the other types have the same name in their abstract version - # as in their db-specific version, so no reverse mapping is needed. + + # Boolean and the SERIAL fields are handled in column_info_to_column, + # and so don't have an entry here. + TINYINT => 'INT1', + SMALLINT => 'INT2', + MEDIUMINT => 'INT3', + INTEGER => 'INT4', + + # All the other types have the same name in their abstract version + # as in their db-specific version, so no reverse mapping is needed. }; -use constant MYISAM_TABLES => qw(bugs_fulltext); +use constant MYISAM_TABLES => qw(); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; - $self = $self->SUPER::_initialize(@_); + $self = $self->SUPER::_initialize(@_); - $self->{db_specific} = { + $self->{db_specific} = { - BOOLEAN => 'tinyint', - FALSE => '0', - TRUE => '1', + BOOLEAN => 'tinyint', + FALSE => '0', + TRUE => '1', - INT1 => 'tinyint', - INT2 => 'smallint', - INT3 => 'mediumint', - INT4 => 'integer', + INT1 => 'tinyint', + INT2 => 'smallint', + INT3 => 'mediumint', + INT4 => 'integer', - SMALLSERIAL => 'smallint auto_increment', - MEDIUMSERIAL => 'mediumint auto_increment', - INTSERIAL => 'integer auto_increment', + SMALLSERIAL => 'smallint auto_increment', + MEDIUMSERIAL => 'mediumint auto_increment', + INTSERIAL => 'integer auto_increment', - TINYTEXT => 'tinytext', - MEDIUMTEXT => 'mediumtext', - LONGTEXT => 'mediumtext', + TINYTEXT => 'tinytext', + MEDIUMTEXT => 'mediumtext', + LONGTEXT => 'mediumtext', - LONGBLOB => 'longblob', + LONGBLOB => 'longblob', - DATETIME => 'datetime', - DATE => 'date', - }; + DATETIME => 'datetime', + DATE => 'date', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; + +} #eosub--_initialize -} #eosub--_initialize #------------------------------------------------------------------------------ sub _get_create_table_ddl { - # Extend superclass method to specify the MYISAM storage engine. - # Returns a "create table" SQL statement. - my($self, $table) = @_; + # Extend superclass method to specify the MYISAM storage engine. + # Returns a "create table" SQL statement. + + my ($self, $table) = @_; - my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; - my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; - return($self->SUPER::_get_create_table_ddl($table) - . " ENGINE = $type $charset"); + my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; + my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; + + my $ddl = $self->SUPER::_get_create_table_ddl($table); + $ddl =~ s/CREATE TABLE (.*) \(/CREATE TABLE `$1` (/; + $ddl .= " ENGINE = $type $charset"; + + return $ddl; + +} #eosub--_get_create_table_ddl -} #eosub--_get_create_table_ddl #------------------------------------------------------------------------------ sub _get_create_index_ddl { - # Extend superclass method to create FULLTEXT indexes on text fields. - # Returns a "create index" SQL statement. - my($self, $table_name, $index_name, $index_fields, $index_type) = @_; + # Extend superclass method to create FULLTEXT indexes on text fields. + # Returns a "create index" SQL statement. + + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type eq 'UNIQUE' - || $index_type eq 'FULLTEXT'); - $sql .= "INDEX \`$index_name\` ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + my $sql = "CREATE "; + $sql .= "$index_type " + if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT'); + $sql .= "INDEX \`$index_name\` ON \`$table_name\` \(" + . join(", ", @$index_fields) . "\)"; - return($sql); + return ($sql); + +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; + return ("CREATE DATABASE $name $charset"); } # MySQL has a simpler ALTER TABLE syntax than ANSI. sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $old_def = $self->get_column($table, $column); - my %new_def_copy = %$new_def; - if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - # If a column stays a primary key do NOT specify PRIMARY KEY in the - # ALTER TABLE statement. This avoids a MySQL error that two primary - # keys are not allowed. - delete $new_def_copy{PRIMARYKEY}; - } - - my @statements; - - push(@statements, "UPDATE $table SET $column = $set_nulls_to - WHERE $column IS NULL") if defined $set_nulls_to; - - # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling - # CHANGE COLUMN, so just do that if we're just changing the default. - my %old_defaultless = %$old_def; - my %new_defaultless = %$new_def; - delete $old_defaultless{DEFAULT}; - delete $new_defaultless{DEFAULT}; - if (!$self->columns_equal($old_def, $new_def) - && $self->columns_equal(\%new_defaultless, \%old_defaultless)) - { - if (!defined $new_def->{DEFAULT}) { - push(@statements, - "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT"); - } - else { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT " . $new_def->{DEFAULT}); - } + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $old_def = $self->get_column($table, $column); + my %new_def_copy = %$new_def; + if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + + # If a column stays a primary key do NOT specify PRIMARY KEY in the + # ALTER TABLE statement. This avoids a MySQL error that two primary + # keys are not allowed. + delete $new_def_copy{PRIMARYKEY}; + } + + my @statements; + + push( + @statements, "UPDATE $table SET $column = $set_nulls_to + WHERE $column IS NULL" + ) if defined $set_nulls_to; + + # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling + # CHANGE COLUMN, so just do that if we're just changing the default. + my %old_defaultless = %$old_def; + my %new_defaultless = %$new_def; + delete $old_defaultless{DEFAULT}; + delete $new_defaultless{DEFAULT}; + if (!$self->columns_equal($old_def, $new_def) + && $self->columns_equal(\%new_defaultless, \%old_defaultless)) + { + if (!defined $new_def->{DEFAULT}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT"); } else { - my $new_ddl = $self->get_type_ddl(\%new_def_copy); - push(@statements, "ALTER TABLE $table CHANGE COLUMN - $column $column $new_ddl"); - } - - if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { - # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT " . $new_def->{DEFAULT} + ); } - - return @statements; + } + else { + my $new_ddl = $self->get_type_ddl(\%new_def_copy); + push( + @statements, "ALTER TABLE $table CHANGE COLUMN + $column $column $new_ddl" + ); + } + + if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + + # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); - my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); - my $dbh = Bugzilla->dbh; - - # MySQL requires, and will create, an index on any column with - # an FK. It will name it after the fk, which we never do. - # So if there's an index named after the fk, we also have to delete it. - if ($dbh->bz_index_info_real($table, $fk_name)) { - push(@sql, $self->get_drop_index_ddl($table, $fk_name)); - } - - return @sql; + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); + my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); + my $dbh = Bugzilla->dbh; + + # MySQL requires, and will create, an index on any column with + # an FK. It will name it after the fk, which we never do. + # So if there's an index named after the fk, we also have to delete it. + if ($dbh->bz_index_info_real($table, $fk_name)) { + push(@sql, $self->get_drop_index_ddl($table, $fk_name)); + } + + return @sql; } sub get_drop_index_ddl { - my ($self, $table, $name) = @_; - return ("DROP INDEX \`$name\` ON $table"); + my ($self, $table, $name) = @_; + return ("DROP INDEX \`$name\` ON $table"); } # A special function for MySQL, for renaming a lot of indexes. -# Index renames is a hash, where the key is a string - the +# Index renames is a hash, where the key is a string - the # old names of the index, and the value is a hash - the index # definition that we're renaming to, with an extra key of "NAME" # that contains the new index name. # The indexes in %indexes must be in hashref format. sub get_rename_indexes_ddl { - my ($self, $table, %indexes) = @_; - my @keys = keys %indexes or return (); - - my $sql = "ALTER TABLE $table "; - - foreach my $old_name (@keys) { - my $name = $indexes{$old_name}->{NAME}; - my $type = $indexes{$old_name}->{TYPE}; - $type ||= 'INDEX'; - my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); - # $old_name needs to be escaped, sometimes, because it was - # a reserved word. - $old_name = '`' . $old_name . '`'; - $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; - } - # Remove the last comma. - chop($sql); - return ($sql); + my ($self, $table, %indexes) = @_; + my @keys = keys %indexes or return (); + + my $sql = "ALTER TABLE $table "; + + foreach my $old_name (@keys) { + my $name = $indexes{$old_name}->{NAME}; + my $type = $indexes{$old_name}->{TYPE}; + $type ||= 'INDEX'; + my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); + + # $old_name needs to be escaped, sometimes, because it was + # a reserved word. + $old_name = '`' . $old_name . '`'; + $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; + } + + # Remove the last comma. + chop($sql); + return ($sql); } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("ALTER TABLE $table AUTO_INCREMENT = $value"); + my ($self, $table, $column, $value) = @_; + return ("ALTER TABLE $table AUTO_INCREMENT = $value"); } # Converts a DBI column_info output to an abstract column definition. @@ -263,124 +291,137 @@ sub get_set_serial_sql { # although there's a chance that it will also work properly if called # elsewhere. sub column_info_to_column { - my ($self, $column_info) = @_; - - # Unfortunately, we have to break Schema's normal "no database" - # barrier a few times in this function. - my $dbh = Bugzilla->dbh; - - my $table = $column_info->{TABLE_NAME}; - my $col_name = $column_info->{COLUMN_NAME}; - - my $column = {}; - - ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - - if ($column_info->{mysql_is_pri_key}) { - # In MySQL, if a table has no PK, but it has a UNIQUE index, - # that index will show up as the PK. So we have to eliminate - # that possibility. - # Unfortunately, the only way to definitely solve this is - # to break Schema's standard of not touching the live database - # and check if the index called PRIMARY is on that field. - my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); - if ( $pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}}) ) { - $column->{PRIMARYKEY} = 1; - } - } + my ($self, $column_info) = @_; - # MySQL frequently defines a default for a field even when we - # didn't explicitly set one. So we have to have some special - # hacks to determine whether or not we should actually put - # a default in the abstract schema for this field. - if (defined $column_info->{COLUMN_DEF}) { - # The defaults that MySQL inputs automatically are usually - # something that would be considered "false" by perl, either - # a 0 or an empty string. (Except for datetime and decimal - # fields, which have their own special auto-defaults.) - # - # Here's how we handle this: If it exists in the schema - # without a default, then we don't use the default. If it - # doesn't exist in the schema, then we're either going to - # be dropping it soon, or it's a custom end-user column, in which - # case having a bogus default won't harm anything. - my $schema_column = $self->get_column($table, $col_name); - unless ( (!$column_info->{COLUMN_DEF} - || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' - || $column_info->{COLUMN_DEF} eq '0.00') - && $schema_column - && !exists $schema_column->{DEFAULT}) { - - my $default = $column_info->{COLUMN_DEF}; - # Schema uses '0' for the defaults for decimal fields. - $default = 0 if $default =~ /^0\.0+$/; - # If we're not a number, we're a string and need to be - # quoted. - $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); - $column->{DEFAULT} = $default; - } - } + # Unfortunately, we have to break Schema's normal "no database" + # barrier a few times in this function. + my $dbh = Bugzilla->dbh; - my $type = $column_info->{TYPE_NAME}; + my $table = $column_info->{TABLE_NAME}; + my $col_name = $column_info->{COLUMN_NAME}; - # Certain types of columns need the size/precision appended. - if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { - # This is nicely lowercase and has the size/precision appended. - $type = $column_info->{mysql_type_name}; - } + my $column = {}; - # If we're a tinyint, we could be either a BOOLEAN or an INT1. - # Only the BOOLEAN_MAP knows the difference. - elsif ($type eq 'TINYINT' && exists BOOLEAN_MAP->{$table} - && exists BOOLEAN_MAP->{$table}->{$col_name}) { - $type = 'BOOLEAN'; - if (exists $column->{DEFAULT}) { - $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; - } - } + ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - # We also need to check if we're an auto_increment field. - elsif ($type =~ /INT/) { - # Unfortunately, the only way to do this in DBI is to query the - # database, so we have to break the rule here that Schema normally - # doesn't touch the live DB. - my $ref_sth = $dbh->prepare( - "SELECT $col_name FROM $table LIMIT 1"); - $ref_sth->execute; - if ($ref_sth->{mysql_is_auto_increment}->[0]) { - if ($type eq 'MEDIUMINT') { - $type = 'MEDIUMSERIAL'; - } - elsif ($type eq 'SMALLINT') { - $type = 'SMALLSERIAL'; - } - else { - $type = 'INTSERIAL'; - } - } - $ref_sth->finish; + if ($column_info->{mysql_is_pri_key}) { + # In MySQL, if a table has no PK, but it has a UNIQUE index, + # that index will show up as the PK. So we have to eliminate + # that possibility. + # Unfortunately, the only way to definitely solve this is + # to break Schema's standard of not touching the live database + # and check if the index called PRIMARY is on that field. + my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); + if ($pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}})) { + $column->{PRIMARYKEY} = 1; } + } + + # MySQL frequently defines a default for a field even when we + # didn't explicitly set one. So we have to have some special + # hacks to determine whether or not we should actually put + # a default in the abstract schema for this field. + if (defined $column_info->{COLUMN_DEF}) { + + # The defaults that MySQL inputs automatically are usually + # something that would be considered "false" by perl, either + # a 0 or an empty string. (Except for datetime and decimal + # fields, which have their own special auto-defaults.) + # + # Here's how we handle this: If it exists in the schema + # without a default, then we don't use the default. If it + # doesn't exist in the schema, then we're either going to + # be dropping it soon, or it's a custom end-user column, in which + # case having a bogus default won't harm anything. + my $schema_column = $self->get_column($table, $col_name); + unless ( + ( + !$column_info->{COLUMN_DEF} + || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' + || $column_info->{COLUMN_DEF} eq '0.00' + ) + && $schema_column + && !exists $schema_column->{DEFAULT} + ) + { + + my $default = $column_info->{COLUMN_DEF}; - # For all other db-specific types, check if they exist in - # REVERSE_MAPPING and use the type found there. - if (exists REVERSE_MAPPING->{$type}) { - $type = REVERSE_MAPPING->{$type}; + # Schema uses '0' for the defaults for decimal fields. + $default = 0 if $default =~ /^0\.0+$/; + + # If we're not a number, we're a string and need to be + # quoted. + $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); + $column->{DEFAULT} = $default; + } + } + + my $type = $column_info->{TYPE_NAME}; + + # Certain types of columns need the size/precision appended. + if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { + + # This is nicely lowercase and has the size/precision appended. + $type = $column_info->{mysql_type_name}; + } + + # If we're a tinyint, we could be either a BOOLEAN or an INT1. + # Only the BOOLEAN_MAP knows the difference. + elsif ($type eq 'TINYINT' + && exists BOOLEAN_MAP->{$table} + && exists BOOLEAN_MAP->{$table}->{$col_name}) + { + $type = 'BOOLEAN'; + if (exists $column->{DEFAULT}) { + $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; } + } + + # We also need to check if we're an auto_increment field. + elsif ($type =~ /INT/) { + + # Unfortunately, the only way to do this in DBI is to query the + # database, so we have to break the rule here that Schema normally + # doesn't touch the live DB. + my $ref_sth = $dbh->prepare("SELECT $col_name FROM $table LIMIT 1"); + $ref_sth->execute; + if ($ref_sth->{mysql_is_auto_increment}->[0]) { + if ($type eq 'MEDIUMINT') { + $type = 'MEDIUMSERIAL'; + } + elsif ($type eq 'SMALLINT') { + $type = 'SMALLSERIAL'; + } + else { + $type = 'INTSERIAL'; + } + } + $ref_sth->finish; + + } - $column->{TYPE} = $type; + # For all other db-specific types, check if they exist in + # REVERSE_MAPPING and use the type found there. + if (exists REVERSE_MAPPING->{$type}) { + $type = REVERSE_MAPPING->{$type}; + } - #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + $column->{TYPE} = $type; - return $column; + #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + + return $column; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_type_ddl($self->get_column($table, $old_name)); - # MySQL doesn't like having the PRIMARY KEY statement in a rename. - $def =~ s/PRIMARY KEY//i; - return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_type_ddl($self->get_column($table, $old_name)); + + # MySQL doesn't like having the PRIMARY KEY statement in a rename. + $def =~ s/PRIMARY KEY//i; + return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); } 1; diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm index 8fb5479b1..416e9204b 100644 --- a/Bugzilla/DB/Schema/Oracle.pm +++ b/Bugzilla/DB/Schema/Oracle.pm @@ -21,8 +21,9 @@ use parent qw(Bugzilla::DB::Schema); use Carp qw(confess); use Bugzilla::Util; -use constant ADD_COLUMN => 'ADD'; +use constant ADD_COLUMN => 'ADD'; use constant MULTIPLE_FKS_IN_ALTER => 0; + # Whether this is true or not, this is what it needs to be in order for # hash_identifier to maintain backwards compatibility with versions before # 3.2rc2. @@ -31,123 +32,128 @@ use constant MAX_IDENTIFIER_LEN => 27; #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + SMALLSERIAL => 'integer', + MEDIUMSERIAL => 'integer', + INTSERIAL => 'integer', - SMALLSERIAL => 'integer', - MEDIUMSERIAL => 'integer', - INTSERIAL => 'integer', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'varchar(4000)', + LONGTEXT => 'clob', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'varchar(4000)', - LONGTEXT => 'clob', + LONGBLOB => 'blob', - LONGBLOB => 'blob', + DATETIME => 'date', + DATE => 'date', + }; - DATETIME => 'date', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_table_ddl { - my $self = shift; - my $table = shift; - unshift @_, $table; - my @ddl = $self->SUPER::get_table_ddl(@_); - - my @fields = @{ $self->{abstract_schema}{$table}{FIELDS} || [] }; - while (@fields) { - my $field_name = shift @fields; - my $field_info = shift @fields; - # Create triggers to deal with empty string. - if ( $field_info->{TYPE} =~ /varchar|TEXT/i - && $field_info->{NOTNULL} ) { - push (@ddl, _get_notnull_trigger_ddl($table, $field_name)); - } - # Create sequences and triggers to emulate SERIAL datatypes. - if ( $field_info->{TYPE} =~ /SERIAL/i ) { - push (@ddl, $self->_get_create_seq_ddl($table, $field_name)); - } + my $self = shift; + my $table = shift; + unshift @_, $table; + my @ddl = $self->SUPER::get_table_ddl(@_); + + my @fields = @{$self->{abstract_schema}{$table}{FIELDS} || []}; + while (@fields) { + my $field_name = shift @fields; + my $field_info = shift @fields; + + # Create triggers to deal with empty string. + if ($field_info->{TYPE} =~ /varchar|TEXT/i && $field_info->{NOTNULL}) { + push(@ddl, _get_notnull_trigger_ddl($table, $field_name)); } - return @ddl; -} #eosub--get_table_ddl + # Create sequences and triggers to emulate SERIAL datatypes. + if ($field_info->{TYPE} =~ /SERIAL/i) { + push(@ddl, $self->_get_create_seq_ddl($table, $field_name)); + } + } + return @ddl; -# Extend superclass method to create Oracle Text indexes if index type +} #eosub--get_table_ddl + +# Extend superclass method to create Oracle Text indexes if index type # is FULLTEXT from schema. Returns a "create index" SQL statement. sub _get_create_index_ddl { - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; - $index_name = "idx_" . $self->_hash_identifier($index_name); - if ($index_type eq 'FULLTEXT') { - my $sql = "CREATE INDEX $index_name ON $table_name (" - . join(',',@$index_fields) - . ") INDEXTYPE IS CTXSYS.CONTEXT " - . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')" ; - return $sql; - } - - return($self->SUPER::_get_create_index_ddl($table_name, $index_name, - $index_fields, $index_type)); + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + $index_name = "idx_" . $self->_hash_identifier($index_name); + if ($index_type eq 'FULLTEXT') { + my $sql + = "CREATE INDEX $index_name ON $table_name (" + . join(',', @$index_fields) + . ") INDEXTYPE IS CTXSYS.CONTEXT " + . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')"; + return $sql; + } + + return ($self->SUPER::_get_create_index_ddl( + $table_name, $index_name, $index_fields, $index_type + )); } sub get_drop_index_ddl { - my $self = shift; - my ($table, $name) = @_; + my $self = shift; + my ($table, $name) = @_; - $name = 'idx_' . $self->_hash_identifier($name); - return $self->SUPER::get_drop_index_ddl($table, $name); + $name = 'idx_' . $self->_hash_identifier($name); + return $self->SUPER::get_drop_index_ddl($table, $name); } -# Oracle supports the use of FOREIGN KEY integrity constraints +# Oracle supports the use of FOREIGN KEY integrity constraints # to define the referential integrity actions, including: # - Update and delete No Action (default) # - Delete CASCADE # - Delete SET NULL sub get_fk_ddl { - my $self = shift; - my $ddl = $self->SUPER::get_fk_ddl(@_); + my $self = shift; + my $ddl = $self->SUPER::get_fk_ddl(@_); - # iThe Bugzilla Oracle driver implements UPDATE via a trigger. - $ddl =~ s/ON UPDATE \S+//i; - # RESTRICT is the default for DELETE on Oracle and may not be specified. - $ddl =~ s/ON DELETE RESTRICT//i; + # iThe Bugzilla Oracle driver implements UPDATE via a trigger. + $ddl =~ s/ON UPDATE \S+//i; - return $ddl; + # RESTRICT is the default for DELETE on Oracle and may not be specified. + $ddl =~ s/ON DELETE RESTRICT//i; + + return $ddl; } sub get_add_fks_sql { - my $self = shift; - my ($table, $column_fks) = @_; - my @sql = $self->SUPER::get_add_fks_sql(@_); - - foreach my $column (keys %$column_fks) { - my $fk = $column_fks->{$column}; - next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; - my $fk_name = $self->_get_fk_name($table, $column, $fk); - my $to_column = $fk->{COLUMN}; - my $to_table = $fk->{TABLE}; - - my $trigger = <<END; + my $self = shift; + my ($table, $column_fks) = @_; + my @sql = $self->SUPER::get_add_fks_sql(@_); + + foreach my $column (keys %$column_fks) { + my $fk = $column_fks->{$column}; + next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; + my $fk_name = $self->_get_fk_name($table, $column, $fk); + my $to_column = $fk->{COLUMN}; + my $to_table = $fk->{TABLE}; + + my $trigger = <<END; CREATE OR REPLACE TRIGGER ${fk_name}_UC AFTER UPDATE OF $to_column ON $to_table REFERENCING NEW AS NEW OLD AS OLD @@ -158,351 +164,371 @@ CREATE OR REPLACE TRIGGER ${fk_name}_UC WHERE $column = :OLD.$to_column; END ${fk_name}_UC; END - push(@sql, $trigger); - } + push(@sql, $trigger); + } - return @sql; + return @sql; } sub get_drop_fk_sql { - my $self = shift; - my ($table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name(@_); - my @sql; - if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { - push(@sql, "DROP TRIGGER ${fk_name}_uc"); - } - push(@sql, $self->SUPER::get_drop_fk_sql(@_)); - return @sql; + my $self = shift; + my ($table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name(@_); + my @sql; + if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { + push(@sql, "DROP TRIGGER ${fk_name}_uc"); + } + push(@sql, $self->SUPER::get_drop_fk_sql(@_)); + return @sql; } sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = "${table}_${column}_${to_table}_${to_column}"; - $fk_name = "fk_" . $self->_hash_identifier($fk_name); - - return $fk_name; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = "${table}_${column}_${to_table}_${to_column}"; + $fk_name = "fk_" . $self->_hash_identifier($fk_name); + + return $fk_name; } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - my @sql; - - # Create sequences and triggers to emulate SERIAL datatypes. - if ($definition->{TYPE} =~ /SERIAL/i) { - # Clone the definition to not alter the original one. - my %def = %$definition; - # Oracle requires to define the column is several steps. - my $pk = delete $def{PRIMARYKEY}; - my $notnull = delete $def{NOTNULL}; - @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); - push(@sql, $self->_get_create_seq_ddl($table, $column)); - push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); - push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; - push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; - } - else { - @sql = $self->SUPER::get_add_column_ddl(@_); - # Create triggers to deal with empty string. - if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($table, $column)); - } + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + my @sql; + + # Create sequences and triggers to emulate SERIAL datatypes. + if ($definition->{TYPE} =~ /SERIAL/i) { + + # Clone the definition to not alter the original one. + my %def = %$definition; + + # Oracle requires to define the column is several steps. + my $pk = delete $def{PRIMARYKEY}; + my $notnull = delete $def{NOTNULL}; + @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); + push(@sql, $self->_get_create_seq_ddl($table, $column)); + push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); + push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; + push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; + } + else { + @sql = $self->SUPER::get_add_column_ddl(@_); + + # Create triggers to deal with empty string. + if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $column)); } + } - return @sql; + return @sql; } sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " DEFAULT NULL"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table MODIFY $column " - . " DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - my $setdefault; - # Handle any fields that were NULL before, if we have a default, - $setdefault = $default if defined $default; - # But if we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $setdefault = $set_nulls_to if defined $set_nulls_to; - if (defined $setdefault) { - push(@statements, "UPDATE $table SET $column = $setdefault" - . " WHERE $column IS NULL"); - } - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NOT NULL"); - push (@statements, _get_notnull_trigger_ddl($table, $column)) - if $old_def->{TYPE} =~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i; - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NULL"); - push(@statements, "DROP TRIGGER ${table}_${column}") - if $new_def->{TYPE} =~ /varchar|text/i - && $old_def->{TYPE} =~ /varchar|text/i; - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " DEFAULT NULL"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, "ALTER TABLE $table MODIFY $column " . " DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + my $setdefault; + + # Handle any fields that were NULL before, if we have a default, + $setdefault = $default if defined $default; + + # But if we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $setdefault = $set_nulls_to if defined $set_nulls_to; + if (defined $setdefault) { + push(@statements, + "UPDATE $table SET $column = $setdefault" . " WHERE $column IS NULL"); } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + push(@statements, "ALTER TABLE $table MODIFY $column" . " NOT NULL"); + push(@statements, _get_notnull_trigger_ddl($table, $column)) + if $old_def->{TYPE} =~ /varchar|text/i && $new_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " NULL"); + push(@statements, "DROP TRIGGER ${table}_${column}") + if $new_def->{TYPE} =~ /varchar|text/i && $old_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) + || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i)) + { + # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, + # just a way to work around. + # Determine whether column_temp is already exist. + my $dbh = Bugzilla->dbh; + my $column_exist = $dbh->selectcol_arrayref( + "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND + CNAME = UPPER(?)", undef, $table, $column . "_temp" + ); + if (!@$column_exist) { + push(@statements, "ALTER TABLE $table ADD ${column}_temp $type"); } - - if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) - || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i) - ) { - # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, - # just a way to work around. - # Determine whether column_temp is already exist. - my $dbh=Bugzilla->dbh; - my $column_exist = $dbh->selectcol_arrayref( - "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND - CNAME = UPPER(?)", undef,$table,$column . "_temp"); - if(!@$column_exist) { - push(@statements, - "ALTER TABLE $table ADD ${column}_temp $type"); - } - push(@statements, "UPDATE $table SET ${column}_temp = $column"); - push(@statements, "COMMIT"); - push(@statements, "ALTER TABLE $table DROP COLUMN $column"); - push(@statements, - "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); - } else { - push(@statements, "ALTER TABLE $table MODIFY $column $type"); - } - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, _get_create_seq_ddl($table, $column)); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@statements, "DROP TRIGGER ${table}_${column}_TR"); - } - - # If this column is changed to type TEXT/VARCHAR, we need to deal with - # empty string. - if ( $old_def->{TYPE} !~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i - && $new_def->{NOTNULL} ) - { - push (@statements, _get_notnull_trigger_ddl($table, $column)); - } - # If this column is no longer TEXT/VARCHAR, we need to drop the trigger - # that went along with it. - if ( $old_def->{TYPE} =~ /varchar|text/i - && $old_def->{NOTNULL} - && $new_def->{TYPE} !~ /varchar|text/i ) - { - push(@statements, "DROP TRIGGER ${table}_${column}"); - } - return @statements; + push(@statements, "UPDATE $table SET ${column}_temp = $column"); + push(@statements, "COMMIT"); + push(@statements, "ALTER TABLE $table DROP COLUMN $column"); + push(@statements, "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); + } + else { + push(@statements, "ALTER TABLE $table MODIFY $column $type"); + } + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push(@statements, _get_create_seq_ddl($table, $column)); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@statements, "DROP TRIGGER ${table}_${column}_TR"); + } + + # If this column is changed to type TEXT/VARCHAR, we need to deal with + # empty string. + if ( $old_def->{TYPE} !~ /varchar|text/i + && $new_def->{TYPE} =~ /varchar|text/i + && $new_def->{NOTNULL}) + { + push(@statements, _get_notnull_trigger_ddl($table, $column)); + } + + # If this column is no longer TEXT/VARCHAR, we need to drop the trigger + # that went along with it. + if ( $old_def->{TYPE} =~ /varchar|text/i + && $old_def->{NOTNULL} + && $new_def->{TYPE} !~ /varchar|text/i) + { + push(@statements, "DROP TRIGGER ${table}_${column}"); + } + return @statements; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also, and fix the default of the series. - my $old_seq = "${table}_${old_name}_SEQ"; - my $new_seq = "${table}_${new_name}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); - push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL} ) { - push(@sql, _get_notnull_trigger_ddl($table,$new_name)); - push(@sql, "DROP TRIGGER ${table}_${old_name}"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also, and fix the default of the series. + my $old_seq = "${table}_${old_name}_SEQ"; + my $new_seq = "${table}_${new_name}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); + push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $new_name)); + push(@sql, "DROP TRIGGER ${table}_${old_name}"); + } + return @sql; } sub get_drop_column_ddl { - my $self = shift; - my ($table, $column) = @_; - my @sql; - push(@sql, $self->SUPER::get_drop_column_ddl(@_)); - my $dbh=Bugzilla->dbh; - my $trigger_name = uc($table . "_" . $column); - my $exist_trigger = $dbh->selectcol_arrayref( - "SELECT OBJECT_NAME FROM USER_OBJECTS - WHERE OBJECT_NAME = ?", undef, $trigger_name); - if(@$exist_trigger) { - push(@sql, "DROP TRIGGER $trigger_name"); - } - # If this column is of type SERIAL, we need to drop the sequence - # and trigger that went along with it. - my $def = $self->get_column_abstract($table, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@sql, "DROP TRIGGER ${table}_${column}_TR"); - } - return @sql; + my $self = shift; + my ($table, $column) = @_; + my @sql; + push(@sql, $self->SUPER::get_drop_column_ddl(@_)); + my $dbh = Bugzilla->dbh; + my $trigger_name = uc($table . "_" . $column); + my $exist_trigger = $dbh->selectcol_arrayref( + "SELECT OBJECT_NAME FROM USER_OBJECTS + WHERE OBJECT_NAME = ?", undef, $trigger_name + ); + if (@$exist_trigger) { + push(@sql, "DROP TRIGGER $trigger_name"); + } + + # If this column is of type SERIAL, we need to drop the sequence + # and trigger that went along with it. + my $def = $self->get_column_abstract($table, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@sql, "DROP TRIGGER ${table}_${column}_TR"); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to rename the sequence. - my $old_seq = "${old_name}_${column}_SEQ"; - my $new_seq = "${new_name}_${column}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); - push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($new_name, $column)); - push(@sql, "DROP TRIGGER ${old_name}_${column}"); - } + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to rename the sequence. + my $old_seq = "${old_name}_${column}_SEQ"; + my $new_seq = "${new_name}_${column}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); + push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($new_name, $column)); + push(@sql, "DROP TRIGGER ${old_name}_${column}"); } + } - return @sql; + return @sql; } sub get_drop_table_ddl { - my ($self, $name) = @_; - my @sql; - - my @columns = $self->get_table_columns($name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to remove the sequence. - push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); - } + my ($self, $name) = @_; + my @sql; + + my @columns = $self->get_table_columns($name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to remove the sequence. + push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); } - push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); + } + push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); - return @sql; + return @sql; } sub _get_notnull_trigger_ddl { - my ($table, $column) = @_; - - my $notnull_sql = "CREATE OR REPLACE TRIGGER " - . " ${table}_${column}" - . " BEFORE INSERT OR UPDATE ON ". $table - . " FOR EACH ROW" - . " BEGIN " - . " IF :NEW.". $column ." IS NULL THEN " - . " SELECT '" . Bugzilla::DB::Oracle->EMPTY_STRING - . "' INTO :NEW.". $column ." FROM DUAL; " - . " END IF; " - . " END ".$table.";"; - return $notnull_sql; + my ($table, $column) = @_; + + my $notnull_sql + = "CREATE OR REPLACE TRIGGER " + . " ${table}_${column}" + . " BEFORE INSERT OR UPDATE ON " + . $table + . " FOR EACH ROW" + . " BEGIN " + . " IF :NEW." + . $column + . " IS NULL THEN " + . " SELECT '" + . Bugzilla::DB::Oracle->EMPTY_STRING + . "' INTO :NEW." + . $column + . " FROM DUAL; " + . " END IF; " . " END " + . $table . ";"; + return $notnull_sql; } sub _get_create_seq_ddl { - my ($self, $table, $column, $start_with) = @_; - $start_with ||= 1; - my @ddl; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name " - . " INCREMENT BY 1 " - . " START WITH $start_with " - . " NOMAXVALUE " - . " NOCYCLE " - . " NOCACHE"; - push (@ddl, $seq_sql); - push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); - - return @ddl; + my ($self, $table, $column, $start_with) = @_; + $start_with ||= 1; + my @ddl; + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql + = "CREATE SEQUENCE $seq_name " + . " INCREMENT BY 1 " + . " START WITH $start_with " + . " NOMAXVALUE " + . " NOCYCLE " + . " NOCACHE"; + push(@ddl, $seq_sql); + push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); + + return @ddl; } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; - my $serial_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $serial_sql; + my ($self, $table, $column, $seq_name) = @_; + my $serial_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $serial_sql; } -sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - my @sql; - my $seq_name = "${table}_${column}_SEQ"; - push(@sql, "DROP SEQUENCE ${seq_name}"); - push(@sql, $self->_get_create_seq_ddl($table, $column, $value)); - return @sql; -} +sub get_set_serial_sql { + my ($self, $table, $column, $value) = @_; + my @sql; + my $seq_name = "${table}_${column}_SEQ"; + push(@sql, "DROP SEQUENCE ${seq_name}"); + push(@sql, $self->_get_create_seq_ddl($table, $column, $value)); + return @sql; +} 1; diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm index 55a932272..cf28a02d9 100644 --- a/Bugzilla/DB/Schema/Pg.pm +++ b/Bugzilla/DB/Schema/Pg.pm @@ -23,169 +23,191 @@ use Storable qw(dclone); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; - - $self = $self->SUPER::_initialize(@_); - - # Remove FULLTEXT index types from the schemas. - foreach my $table (keys %{ $self->{schema} }) { - if ($self->{schema}{$table}{INDEXES}) { - foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } - foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } + my $self = shift; + + $self = $self->SUPER::_initialize(@_); + + # Remove FULLTEXT index types from the schemas. + foreach my $table (keys %{$self->{schema}}) { + if ($self->{schema}{$table}{INDEXES}) { + foreach my $index (@{$self->{schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); + } + } + foreach my $index (@{$self->{abstract_schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); } + } } + } - $self->{db_specific} = { + $self->{db_specific} = { - BOOLEAN => 'smallint', - FALSE => '0', - TRUE => '1', + BOOLEAN => 'smallint', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'serial unique', - MEDIUMSERIAL => 'serial unique', - INTSERIAL => 'serial unique', + SMALLSERIAL => 'serial unique', + MEDIUMSERIAL => 'serial unique', + INTSERIAL => 'serial unique', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'bytea', + LONGBLOB => 'bytea', - DATETIME => 'timestamp(0) without time zone', - DATE => 'date', - }; + DATETIME => 'timestamp(0) without time zone', + DATE => 'date', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; + +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; + return ("CREATE DATABASE $name $charset"); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also. - push(@sql, "ALTER SEQUENCE ${table}_${old_name}_seq - RENAME TO ${table}_${new_name}_seq"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also. + push( + @sql, "ALTER SEQUENCE ${table}_${old_name}_seq + RENAME TO ${table}_${new_name}_seq" + ); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + + # If there's a SERIAL column on this table, we also need to rename the + # sequence. + # If there is a PRIMARY KEY, we need to rename it too. + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + my $old_seq = "${old_name}_${column}_seq"; + my $new_seq = "${new_name}_${column}_seq"; + push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + push( + @sql, "ALTER TABLE $new_name ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); } - - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - - # If there's a SERIAL column on this table, we also need to rename the - # sequence. - # If there is a PRIMARY KEY, we need to rename it too. - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - my $old_seq = "${old_name}_${column}_seq"; - my $new_seq = "${new_name}_${column}_seq"; - push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - push(@sql, "ALTER TABLE $new_name ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - } - if ($def->{PRIMARYKEY}) { - my $old_pk = "${old_name}_pkey"; - my $new_pk = "${new_name}_pkey"; - push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); - } + if ($def->{PRIMARYKEY}) { + my $old_pk = "${old_name}_pkey"; + my $new_pk = "${new_name}_pkey"; + push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); } + } - return @sql; + return @sql; } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("SELECT setval('${table}_${column}_seq', $value, false) - FROM $table"); + my ($self, $table, $column, $value) = @_; + return ( + "SELECT setval('${table}_${column}_seq', $value, false) + FROM $table" + ); } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; - } - - $type =~ s/\bserial\b/integer/i; - - # On Pg, you don't need UNIQUE if you're a PK--it creates - # two identical indexes otherwise. - $type =~ s/unique//i if $new_def->{PRIMARYKEY}; - - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - TYPE $type"); - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, "CREATE SEQUENCE ${table}_${column}_seq - OWNED BY $table.$column"); - push(@statements, "SELECT setval('${table}_${column}_seq', + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + $type =~ s/\bserial\b/integer/i; + + # On Pg, you don't need UNIQUE if you're a PK--it creates + # two identical indexes otherwise. + $type =~ s/unique//i if $new_def->{PRIMARYKEY}; + + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + TYPE $type" + ); + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push( + @statements, "CREATE SEQUENCE ${table}_${column}_seq + OWNED BY $table.$column" + ); + push( + @statements, "SELECT setval('${table}_${column}_seq', MAX($table.$column)) - FROM $table"); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('${table}_${column}_seq')"); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - DROP DEFAULT"); - push(@statements, "ALTER SEQUENCE ${table}_${column}_seq - OWNED BY NONE"); - push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); - } - - return @statements; + FROM $table" + ); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT nextval('${table}_${column}_seq')" + ); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + DROP DEFAULT" + ); + push( + @statements, "ALTER SEQUENCE ${table}_${column}_seq + OWNED BY NONE" + ); + push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); + } + + return @statements; } 1; diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm index ccdbfd8aa..57361d2bb 100644 --- a/Bugzilla/DB/Schema/Sqlite.pm +++ b/Bugzilla/DB/Schema/Sqlite.pm @@ -22,37 +22,37 @@ use constant FK_ON_CREATE => 1; sub _initialize { - my $self = shift; + my $self = shift; - $self = $self->SUPER::_initialize(@_); + $self = $self->SUPER::_initialize(@_); - $self->{db_specific} = { - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'SERIAL', - MEDIUMSERIAL => 'SERIAL', - INTSERIAL => 'SERIAL', + SMALLSERIAL => 'SERIAL', + MEDIUMSERIAL => 'SERIAL', + INTSERIAL => 'SERIAL', - TINYTEXT => 'text', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'text', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'blob', + LONGBLOB => 'blob', - DATETIME => 'DATETIME', - DATE => 'DATETIME', - }; + DATETIME => 'DATETIME', + DATE => 'DATETIME', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; } @@ -61,83 +61,86 @@ sub _initialize { ################################# sub _sqlite_create_table { - my ($self, $table) = @_; - return scalar Bugzilla->dbh->selectrow_array( - "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", - undef, $table); + my ($self, $table) = @_; + return + scalar Bugzilla->dbh->selectrow_array( + "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", + undef, $table); } sub _sqlite_table_lines { - my $self = shift; - my $table_sql = $self->_sqlite_create_table(@_); - $table_sql =~ s/\n*\)$//s; - # The $ makes this work even if people some day add crazy stuff to their - # schema like multi-column foreign keys. - return split(/,\s*$/m, $table_sql); + my $self = shift; + my $table_sql = $self->_sqlite_create_table(@_); + $table_sql =~ s/\n*\)$//s; + + # The $ makes this work even if people some day add crazy stuff to their + # schema like multi-column foreign keys. + return split(/,\s*$/m, $table_sql); } # This does most of the "heavy lifting" of the schema-altering functions. sub _sqlite_alter_schema { - my ($self, $table, $create_table, $options) = @_; - - # $create_table is sometimes an array in the form that _sqlite_table_lines - # returns. - if (ref $create_table) { - $create_table = join(',', @$create_table) . "\n)"; - } - - my $dbh = Bugzilla->dbh; - - my $random = generate_random_password(5); - my $rename_to = "${table}_$random"; - - my @columns = $dbh->bz_table_columns_real($table); - push(@columns, $options->{extra_column}) if $options->{extra_column}; - if (my $exclude = $options->{exclude_column}) { - @columns = grep { $_ ne $exclude } @columns; - } - my @insert_cols = @columns; - my @select_cols = @columns; - if (my $rename = $options->{rename}) { - foreach my $from (keys %$rename) { - my $to = $rename->{$from}; - @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; - } + my ($self, $table, $create_table, $options) = @_; + + # $create_table is sometimes an array in the form that _sqlite_table_lines + # returns. + if (ref $create_table) { + $create_table = join(',', @$create_table) . "\n)"; + } + + my $dbh = Bugzilla->dbh; + + my $random = generate_random_password(5); + my $rename_to = "${table}_$random"; + + my @columns = $dbh->bz_table_columns_real($table); + push(@columns, $options->{extra_column}) if $options->{extra_column}; + if (my $exclude = $options->{exclude_column}) { + @columns = grep { $_ ne $exclude } @columns; + } + my @insert_cols = @columns; + my @select_cols = @columns; + if (my $rename = $options->{rename}) { + foreach my $from (keys %$rename) { + my $to = $rename->{$from}; + @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; } - - my $insert_str = join(',', @insert_cols); - my $select_str = join(',', @select_cols); - my $copy_sql = "INSERT INTO $table ($insert_str)" - . " SELECT $select_str FROM $rename_to"; - - # We have to turn FKs off before doing this. Otherwise, when we rename - # the table, all of the FKs in the other tables will be automatically - # updated to point to the renamed table. Note that PRAGMA foreign_keys - # can only be set outside of a transaction--otherwise it is a no-op. - if ($dbh->bz_in_transaction) { - die "can't alter the schema inside of a transaction"; - } - my @sql = ( - 'PRAGMA foreign_keys = OFF', - 'BEGIN EXCLUSIVE TRANSACTION', - @{ $options->{pre_sql} || [] }, - "ALTER TABLE $table RENAME TO $rename_to", - $create_table, - $copy_sql, - "DROP TABLE $rename_to", - 'COMMIT TRANSACTION', - 'PRAGMA foreign_keys = ON', - ); + } + + my $insert_str = join(',', @insert_cols); + my $select_str = join(',', @select_cols); + my $copy_sql + = "INSERT INTO $table ($insert_str)" . " SELECT $select_str FROM $rename_to"; + + # We have to turn FKs off before doing this. Otherwise, when we rename + # the table, all of the FKs in the other tables will be automatically + # updated to point to the renamed table. Note that PRAGMA foreign_keys + # can only be set outside of a transaction--otherwise it is a no-op. + if ($dbh->bz_in_transaction) { + die "can't alter the schema inside of a transaction"; + } + my @sql = ( + 'PRAGMA foreign_keys = OFF', + 'BEGIN EXCLUSIVE TRANSACTION', + @{$options->{pre_sql} || []}, + "ALTER TABLE $table RENAME TO $rename_to", + $create_table, + $copy_sql, + "DROP TABLE $rename_to", + 'COMMIT TRANSACTION', + 'PRAGMA foreign_keys = ON', + ); } # For finding a particular column's definition in a CREATE TABLE statement. sub _sqlite_column_regex { - my ($column) = @_; - # 1 = Comma at start - # 2 = Column name + Space - # 3 = Definition - # 4 = Ending comma - return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; + my ($column) = @_; + + # 1 = Comma at start + # 2 = Column name + Space + # 3 = Definition + # 4 = Ending comma + return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; } ############################# @@ -145,133 +148,137 @@ sub _sqlite_column_regex { ############################# sub get_create_database_sql { - # If we get here, it means there was some error creating the - # database file during bz_create_database in Bugzilla::DB, - # and we just want to display that error instead of doing - # anything else. - Bugzilla->dbh; - die "Reached an unreachable point"; + + # If we get here, it means there was some error creating the + # database file during bz_create_database in Bugzilla::DB, + # and we just want to display that error instead of doing + # anything else. + Bugzilla->dbh; + die "Reached an unreachable point"; } sub _get_create_table_ddl { - my $self = shift; - my ($table) = @_; - my $ddl = $self->SUPER::_get_create_table_ddl(@_); - - # TheSchwartz uses its own driver to access its tables, meaning - # that it doesn't understand "COLLATE bugzilla" and in fact - # SQLite throws an error when TheSchwartz tries to access its - # own tables, if COLLATE bugzilla is on them. We don't have - # to fix this elsewhere currently, because we only create - # TheSchwartz's tables, we never modify them. - if ($table =~ /^ts_/) { - $ddl =~ s/ COLLATE bugzilla//g; - } - return $ddl; + my $self = shift; + my ($table) = @_; + my $ddl = $self->SUPER::_get_create_table_ddl(@_); + + # TheSchwartz uses its own driver to access its tables, meaning + # that it doesn't understand "COLLATE bugzilla" and in fact + # SQLite throws an error when TheSchwartz tries to access its + # own tables, if COLLATE bugzilla is on them. We don't have + # to fix this elsewhere currently, because we only create + # TheSchwartz's tables, we never modify them. + if ($table =~ /^ts_/) { + $ddl =~ s/ COLLATE bugzilla//g; + } + return $ddl; } sub get_type_ddl { - my $self = shift; - my $def = dclone($_[0]); - - my $ddl = $self->SUPER::get_type_ddl(@_); - if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { - $ddl =~ s/\bSERIAL\b/integer/; - $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; - } - if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { - $ddl .= " COLLATE bugzilla"; - } - # Don't collate DATETIME fields. - if ($def->{TYPE} eq 'DATETIME') { - $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; - } - return $ddl; + my $self = shift; + my $def = dclone($_[0]); + + my $ddl = $self->SUPER::get_type_ddl(@_); + if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { + $ddl =~ s/\bSERIAL\b/integer/; + $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; + } + if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { + $ddl .= " COLLATE bugzilla"; + } + + # Don't collate DATETIME fields. + if ($def->{TYPE} eq 'DATETIME') { + $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; + } + return $ddl; } sub get_alter_column_ddl { - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - my $dbh = Bugzilla->dbh; - - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($new_def); - # When we do ADD COLUMN, columns can show up all on one line separated - # by commas, so we have to account for that. - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ - || die "couldn't find $column in $table:\n$table_sql"; - my @pre_sql = $self->_set_nulls_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql }); + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + my $dbh = Bugzilla->dbh; + + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($new_def); + + # When we do ADD COLUMN, columns can show up all on one line separated + # by commas, so we have to account for that. + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ + || die "couldn't find $column in $table:\n$table_sql"; + my @pre_sql = $self->_set_nulls_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, {pre_sql => \@pre_sql}); } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - # SQLite can use the normal ADD COLUMN when: - # * The column isn't a PK - if ($definition->{PRIMARYKEY}) { - if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { - die "You can only add new SERIAL type PKs with SQLite"; - } - my $table_sql = $self->_sqlite_new_column_sql(@_); - # This works because _sqlite_alter_schema will exclude the new column - # in its INSERT ... SELECT statement, meaning that when the "new" - # table is populated, it will have AUTOINCREMENT values generated - # for it. - return $self->_sqlite_alter_schema($table, $table_sql); - } - # * The column has a default one way or another. Either it - # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT - # clause. Since we also require this when doing bz_add_column (in - # the way of forcing an init_value for NOT NULL columns with no - # default), we first set the init_value as the default and then - # alter the column. - if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { - my %with_default = %$definition; - $with_default{DEFAULT} = $init_value; - my @pre_sql = - $self->SUPER::get_add_column_ddl($table, $column, \%with_default); - my $table_sql = $self->_sqlite_new_column_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql, extra_column => $column }); + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + + # SQLite can use the normal ADD COLUMN when: + # * The column isn't a PK + if ($definition->{PRIMARYKEY}) { + if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { + die "You can only add new SERIAL type PKs with SQLite"; } - - return $self->SUPER::get_add_column_ddl(@_); + my $table_sql = $self->_sqlite_new_column_sql(@_); + + # This works because _sqlite_alter_schema will exclude the new column + # in its INSERT ... SELECT statement, meaning that when the "new" + # table is populated, it will have AUTOINCREMENT values generated + # for it. + return $self->_sqlite_alter_schema($table, $table_sql); + } + + # * The column has a default one way or another. Either it + # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT + # clause. Since we also require this when doing bz_add_column (in + # the way of forcing an init_value for NOT NULL columns with no + # default), we first set the init_value as the default and then + # alter the column. + if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { + my %with_default = %$definition; + $with_default{DEFAULT} = $init_value; + my @pre_sql = $self->SUPER::get_add_column_ddl($table, $column, \%with_default); + my $table_sql = $self->_sqlite_new_column_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, + {pre_sql => \@pre_sql, extra_column => $column}); + } + + return $self->SUPER::get_add_column_ddl(@_); } sub _sqlite_new_column_sql { - my ($self, $table, $column, $def) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($def); - my $new_line = "\t$column\t$new_ddl"; - $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s - || die "Can't find start of CREATE TABLE:\n$table_sql"; - return $table_sql; + my ($self, $table, $column, $def) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($def); + my $new_line = "\t$column\t$new_ddl"; + $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s + || die "Can't find start of CREATE TABLE:\n$table_sql"; + return $table_sql; } sub get_drop_column_ddl { - my ($self, $table, $column) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1/ - || die "Can't find column $column: $table_sql"; - # Make sure we don't end up with a comma at the end of the definition. - $table_sql =~ s/,\s+\)$/\n)/s; - return $self->_sqlite_alter_schema($table, $table_sql, - { exclude_column => $column }); + my ($self, $table, $column) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1/ + || die "Can't find column $column: $table_sql"; + + # Make sure we don't end up with a comma at the end of the definition. + $table_sql =~ s/,\s+\)$/\n)/s; + return $self->_sqlite_alter_schema($table, $table_sql, + {exclude_column => $column}); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($old_name); - $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ - || die "Can't find $old_name: $table_sql"; - my %rename = ($old_name => $new_name); - return $self->_sqlite_alter_schema($table, $table_sql, - { rename => \%rename }); + my ($self, $table, $old_name, $new_name) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($old_name); + $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ + || die "Can't find $old_name: $table_sql"; + my %rename = ($old_name => $new_name); + return $self->_sqlite_alter_schema($table, $table_sql, {rename => \%rename}); } ################ @@ -279,24 +286,23 @@ sub get_rename_column_ddl { ################ sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my @add = $self->_column_fks_to_ddl($table, $column_fks); - push(@clauses, @add); - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column_fks) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my @add = $self->_column_fks_to_ddl($table, $column_fks); + push(@clauses, @add); + return $self->_sqlite_alter_schema($table, \@clauses); } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my $fk_name = $self->_get_fk_name($table, $column, $references); - - my $line_re = qr/^\s+CONSTRAINT $fk_name /s; - grep { $line_re } @clauses - or die "Can't find $fk_name: " . join(',', @clauses); - @clauses = grep { $_ !~ $line_re } @clauses; - - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column, $references) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my $fk_name = $self->_get_fk_name($table, $column, $references); + + my $line_re = qr/^\s+CONSTRAINT $fk_name /s; + grep {$line_re} @clauses or die "Can't find $fk_name: " . join(',', @clauses); + @clauses = grep { $_ !~ $line_re } @clauses; + + return $self->_sqlite_alter_schema($table, \@clauses); } diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm index a56ed31ad..c180fd0d7 100644 --- a/Bugzilla/DB/Sqlite.pm +++ b/Bugzilla/DB/Sqlite.pm @@ -46,23 +46,23 @@ sub _sqlite_collate_ci { lc($_[0]) cmp lc($_[1]) } sub _sqlite_mod { $_[0] % $_[1] } sub _sqlite_now { - my $now = DateTime->now(time_zone => Bugzilla->local_timezone); - return $now->ymd . ' ' . $now->hms; + my $now = DateTime->now(time_zone => Bugzilla->local_timezone); + return $now->ymd . ' ' . $now->hms; } # SQL's POSITION starts its values from 1 instead of 0 (so we add 1). sub _sqlite_position { - my ($text, $fragment) = @_; - if (!defined $text or !defined $fragment) { - return undef; - } - my $pos = index $text, $fragment; - return $pos + 1; + my ($text, $fragment) = @_; + if (!defined $text or !defined $fragment) { + return undef; + } + my $pos = index $text, $fragment; + return $pos + 1; } sub _sqlite_position_ci { - my ($text, $fragment) = @_; - return _sqlite_position(lc($text), lc($fragment)); + my ($text, $fragment) = @_; + return _sqlite_position(lc($text), lc($fragment)); } ############### @@ -70,76 +70,84 @@ sub _sqlite_position_ci { ############### sub new { - my ($class, $params) = @_; - my $db_name = $params->{db_name}; - - # Let people specify paths intead of data/ for the DB. - if ($db_name and $db_name !~ m{[\\/]}) { - # When the DB is first created, there's a chance that the - # data directory doesn't exist at all, because the Install::Filesystem - # code happens after DB creation. So we create the directory ourselves - # if it doesn't exist. - my $datadir = bz_locations()->{datadir}; - if (!-d $datadir) { - mkdir $datadir or warn "$datadir: $!"; - } - if (!-d "$datadir/db/") { - mkdir "$datadir/db/" or warn "$datadir/db: $!"; - } - $db_name = bz_locations()->{datadir} . "/db/$db_name"; + my ($class, $params) = @_; + my $db_name = $params->{db_name}; + + # Let people specify paths intead of data/ for the DB. + if ($db_name and $db_name !~ m{[\\/]}) { + + # When the DB is first created, there's a chance that the + # data directory doesn't exist at all, because the Install::Filesystem + # code happens after DB creation. So we create the directory ourselves + # if it doesn't exist. + my $datadir = bz_locations()->{datadir}; + if (!-d $datadir) { + mkdir $datadir or warn "$datadir: $!"; } - - # construct the DSN from the parameters we got - my $dsn = "dbi:SQLite:dbname=$db_name"; - - my $attrs = { - # XXX Should we just enforce this to be always on? - sqlite_unicode => Bugzilla->params->{'utf8'}, - }; - - my $self = $class->db_new({ dsn => $dsn, user => '', - pass => '', attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - my %pragmas = ( - # Make sure that the sqlite file doesn't grow without bound. - auto_vacuum => 1, - encoding => "'UTF-8'", - foreign_keys => 'ON', - # We want the latest file format. - legacy_file_format => 'OFF', - # This guarantees that we get column names like "foo" - # instead of "table.foo" in selectrow_hashref. - short_column_names => 'ON', - # The write-ahead log mode in SQLite 3.7 gets us better concurrency, - # but breaks backwards-compatibility with older versions of - # SQLite. (Which is important because people may also want to use - # command-line clients to access and back up their DB.) If you need - # better concurrency and don't need 3.6 compatibility, then you can - # uncomment this line. - #journal_mode => "'WAL'", - ); - - while (my ($name, $value) = each %pragmas) { - $self->do("PRAGMA $name = $value"); + if (!-d "$datadir/db/") { + mkdir "$datadir/db/" or warn "$datadir/db: $!"; } - - $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); - $self->sqlite_create_function('position', 2, \&_sqlite_position); - $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); - # SQLite has a "substr" function, but other DBs call it "SUBSTRING" - # so that's what we use, and I don't know of any way in SQLite to - # alias the SQL "substr" function to be called "SUBSTRING". - $self->sqlite_create_function('substring', 3, \&CORE::substr); - $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); - $self->sqlite_create_function('mod', 2, \&_sqlite_mod); - $self->sqlite_create_function('now', 0, \&_sqlite_now); - $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); - $self->sqlite_create_function('floor', 1, \&POSIX::floor); - - bless ($self, $class); - return $self; + $db_name = bz_locations()->{datadir} . "/db/$db_name"; + } + + # construct the DSN from the parameters we got + my $dsn = "dbi:SQLite:dbname=$db_name"; + + my $attrs = { + + # XXX Should we just enforce this to be always on? + sqlite_unicode => Bugzilla->params->{'utf8'}, + }; + + my $self + = $class->db_new({dsn => $dsn, user => '', pass => '', attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + my %pragmas = ( + + # Make sure that the sqlite file doesn't grow without bound. + auto_vacuum => 1, + encoding => "'UTF-8'", + foreign_keys => 'ON', + + # We want the latest file format. + legacy_file_format => 'OFF', + + # This guarantees that we get column names like "foo" + # instead of "table.foo" in selectrow_hashref. + short_column_names => 'ON', + + # The write-ahead log mode in SQLite 3.7 gets us better concurrency, + # but breaks backwards-compatibility with older versions of + # SQLite. (Which is important because people may also want to use + # command-line clients to access and back up their DB.) If you need + # better concurrency and don't need 3.6 compatibility, then you can + # uncomment this line. + #journal_mode => "'WAL'", + ); + + while (my ($name, $value) = each %pragmas) { + $self->do("PRAGMA $name = $value"); + } + + $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); + $self->sqlite_create_function('position', 2, \&_sqlite_position); + $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); + + # SQLite has a "substr" function, but other DBs call it "SUBSTRING" + # so that's what we use, and I don't know of any way in SQLite to + # alias the SQL "substr" function to be called "SUBSTRING". + $self->sqlite_create_function('substring', 3, \&CORE::substr); + $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); + $self->sqlite_create_function('mod', 2, \&_sqlite_mod); + $self->sqlite_create_function('now', 0, \&_sqlite_now); + $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); + $self->sqlite_create_function('floor', 1, \&POSIX::floor); + + bless($self, $class); + return $self; } ############### @@ -147,86 +155,89 @@ sub new { ############### sub sql_position { - my ($self, $fragment, $text) = @_; - return "POSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "POSITION($text, $fragment)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "IPOSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "IPOSITION($text, $fragment)"; } # SQLite does not have to GROUP BY the optional columns. sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; - my $expression = "GROUP BY $needed_columns"; - return $expression; + my ($self, $needed_columns, $optional_columns) = @_; + my $expression = "GROUP BY $needed_columns"; + return $expression; } # XXX SQLite does not support sorting a GROUP_CONCAT, so $sort is unimplemented. sub sql_group_concat { - my ($self, $column, $separator, $sort) = @_; - $separator = $self->quote(', ') if !defined $separator; - # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't - # specify its separator, and has to accept the default of ",". - if ($column =~ /^DISTINCT/) { - return "GROUP_CONCAT($column)"; - } - return "GROUP_CONCAT($column, $separator)"; + my ($self, $column, $separator, $sort) = @_; + $separator = $self->quote(', ') if !defined $separator; + + # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't + # specify its separator, and has to accept the default of ",". + if ($column =~ /^DISTINCT/) { + return "GROUP_CONCAT($column)"; + } + return "GROUP_CONCAT($column, $separator)"; } sub sql_istring { - my ($self, $string) = @_; - return $string; + my ($self, $string) = @_; + return $string; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my $self = shift; - my $re_expression = $self->sql_regexp(@_); - return "NOT($re_expression)"; + my $self = shift; + my $re_expression = $self->sql_regexp(@_); + return "NOT($re_expression)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; - return "DATETIME($days)"; + my ($self, $days) = @_; + return "DATETIME($days)"; } sub sql_to_days { - my ($self, $date) = @_; - return "JULIANDAY($date)"; + my ($self, $date) = @_; + return "JULIANDAY($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - $format = "%Y.%m.%d %H:%M:%S" if !$format; - $format =~ s/\%i/\%M/g; - $format =~ s/\%s/\%S/g; - return "STRFTIME(" . $self->quote($format) . ", $date)"; + my ($self, $date, $format) = @_; + $format = "%Y.%m.%d %H:%M:%S" if !$format; + $format =~ s/\%i/\%M/g; + $format =~ s/\%s/\%S/g; + return "STRFTIME(" . $self->quote($format) . ", $date)"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - # We do the || thing (concatenation) so that placeholders work properly. - return "DATETIME($date, '$operator' || $interval || ' $units')"; + my ($self, $date, $operator, $interval, $units) = @_; + + # We do the || thing (concatenation) so that placeholders work properly. + return "DATETIME($date, '$operator' || $interval || ' $units')"; } ############### @@ -234,56 +245,57 @@ sub sql_date_math { ############### sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - # If we created TheSchwartz tables with COLLATE bugzilla (during the - # 4.1.x development series) re-create them without it. - my @tables = $self->bz_table_list(); - my @ts_tables = grep { /^ts_/ } @tables; - my $drop_ok; - foreach my $table (@ts_tables) { - my $create_table = - $self->_bz_real_schema->_sqlite_create_table($table); - if ($create_table =~ /COLLATE bugzilla/) { - if (!$drop_ok) { - _sqlite_jobqueue_drop_message(); - $drop_ok = 1; - } - $self->bz_drop_table($table); - $self->bz_add_table($table); - } + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + # If we created TheSchwartz tables with COLLATE bugzilla (during the + # 4.1.x development series) re-create them without it. + my @tables = $self->bz_table_list(); + my @ts_tables = grep {/^ts_/} @tables; + my $drop_ok; + foreach my $table (@ts_tables) { + my $create_table = $self->_bz_real_schema->_sqlite_create_table($table); + if ($create_table =~ /COLLATE bugzilla/) { + if (!$drop_ok) { + _sqlite_jobqueue_drop_message(); + $drop_ok = 1; + } + $self->bz_drop_table($table); + $self->bz_add_table($table); } + } } sub _sqlite_jobqueue_drop_message { - # This is not translated because this situation will only happen if - # you are updating from a 4.1.x development version of Bugzilla using - # SQLite, and we don't want to maintain this string in strings.txt.pl - # forever for just this one uncommon circumstance. - print <<END; + + # This is not translated because this situation will only happen if + # you are updating from a 4.1.x development version of Bugzilla using + # SQLite, and we don't want to maintain this string in strings.txt.pl + # forever for just this one uncommon circumstance. + print <<END; WARNING: We have to re-create all the database tables used by jobqueue.pl. If there are any pending jobs in the database (that is, emails that haven't been sent), they will be deleted. END - unless (Bugzilla->installation_answers->{NO_PAUSE}) { - print install_string('enter_or_ctrl_c'); - getc; - } + unless (Bugzilla->installation_answers->{NO_PAUSE}) { + print install_string('enter_or_ctrl_c'); + getc; + } } # XXX This needs to be implemented. sub bz_explain { } sub bz_table_list_real { - my $self = shift; - my @tables = $self->SUPER::bz_table_list_real(@_); - # SQLite includes a sqlite_sequence table in every database that isn't - # one of our real tables. We exclude any table that starts with sqlite_, - # just to be safe. - @tables = grep { $_ !~ /^sqlite_/ } @tables; - return @tables; + my $self = shift; + my @tables = $self->SUPER::bz_table_list_real(@_); + + # SQLite includes a sqlite_sequence table in every database that isn't + # one of our real tables. We exclude any table that starts with sqlite_, + # just to be safe. + @tables = grep { $_ !~ /^sqlite_/ } @tables; + return @tables; } 1; |