diff options
Diffstat (limited to 'Bugzilla/DB/Pg.pm')
-rw-r--r-- | Bugzilla/DB/Pg.pm | 436 |
1 files changed, 229 insertions, 207 deletions
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm index 0db349412..d1bb0f798 100644 --- a/Bugzilla/DB/Pg.pm +++ b/Bugzilla/DB/Pg.pm @@ -31,151 +31,153 @@ use DBD::Pg; # This module extends the DB interface via inheritance extends qw(Bugzilla::DB); -use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA }; +use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA}; sub BUILDARGS { - 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'}}; - return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs } + return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}; } # 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) = @_; - $sort = 1 if !defined $sort; - $separator = $self->quote(', ') if !defined $separator; - my $sql = "array_accum($text)"; - if ($sort) { - $sql = "array_sort($sql)"; - } - return "array_to_string($sql, $separator)"; + my ($self, $text, $separator, $sort) = @_; + $sort = 1 if !defined $sort; + $separator = $self->quote(', ') if !defined $separator; + my $sql = "array_accum($text)"; + if ($sort) { + $sql = "array_sort($sql)"; + } + return "array_to_string($sql, $separator)"; } 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_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) = @_; + my ($self, $date, $format) = @_; - $format = "%Y.%m.%d %H:%i:%s" if !$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 ($self, $date, $operator, $interval, $units) = @_; - return "$date $operator $interval * INTERVAL '1 $units'"; + return "$date $operator $interval * INTERVAL '1 $units'"; } sub sql_string_concat { - my ($self, @params) = @_; + 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))'; + # 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); } ##################################################################### @@ -183,38 +185,42 @@ 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(@_); - - # Custom Functions - 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(@_); + + # Custom Functions + 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 @@ -228,117 +234,132 @@ SELECT ARRAY( $$; END - # 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); } + } } ##################################################################### @@ -348,12 +369,13 @@ sub _fix_case_differences { # 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; |