use Bugzilla::DB::Schema;
use List::Util qw(max);
+use Storable qw(dclone);
#####################################################################
# Constants
}
}
+sub bz_setup_foreign_keys {
+ my ($self) = @_;
+
+ # We use _bz_schema because bz_add_table has removed all REFERENCES
+ # items from _bz_real_schema.
+ my @tables = $self->_bz_schema->get_table_list();
+ foreach my $table (@tables) {
+ my @columns = $self->_bz_schema->get_table_columns($table);
+ foreach my $column (@columns) {
+ my $def = $self->_bz_schema->get_column_abstract($table, $column);
+ if ($def->{REFERENCES}) {
+ $self->bz_add_fk($table, $column, $def->{REFERENCES});
+ }
+ }
+ }
+}
+
#####################################################################
# Schema Modification Methods
#####################################################################
}
}
+sub bz_add_fk {
+ my ($self, $table, $column, $def) = @_;
+
+ my $col_def = $self->bz_column_info($table, $column);
+ if (!$col_def->{REFERENCES}) {
+ $self->_check_references($table, $column, $def->{TABLE},
+ $def->{COLUMN});
+ print get_text('install_fk_add',
+ { table => $table, column => $column, fk => $def })
+ . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+ my @sql = $self->_bz_real_schema->get_add_fk_sql($table, $column, $def);
+ $self->do($_) foreach @sql;
+ $col_def->{REFERENCES} = $def;
+ $self->_bz_real_schema->set_column($table, $column, $col_def);
+ $self->_bz_store_real_schema;
+ }
+}
+
sub bz_alter_column {
my ($self, $table, $name, $new_def, $set_nulls_to) = @_;
my @statements = $self->_bz_real_schema->get_alter_column_ddl(
$table, $name, $new_def,
defined $set_nulls_to ? $self->quote($set_nulls_to) : undef);
- my $new_ddl = $self->_bz_schema->get_display_ddl($table, $name, $new_def);
+ my $new_ddl = $self->_bz_schema->get_type_ddl($new_def);
print "Updating column $name in table $table ...\n";
if (defined $current_def) {
- my $old_ddl = $self->_bz_schema->get_display_ddl($table, $name,
- $current_def);
+ my $old_ddl = $self->_bz_schema->get_type_ddl($current_def);
print "Old: $old_ddl\n";
}
print "New: $new_ddl\n";
if (!$table_exists) {
$self->_bz_add_table_raw($name);
- $self->_bz_real_schema->add_table($name,
- $self->_bz_schema->get_table_abstract($name));
+ my $table_def = dclone($self->_bz_schema->get_table_abstract($name));
+
+ my %fields = @{$table_def->{FIELDS}};
+ foreach my $col (keys %fields) {
+ # Foreign Key references have to be added by Install::DB after
+ # initial table creation, because column names have changed
+ # over history and it's impossible to keep track of that info
+ # in ABSTRACT_SCHEMA.
+ delete $fields{$col}->{REFERENCES};
+ }
+ $self->_bz_real_schema->add_table($name, $table_def);
$self->_bz_store_real_schema;
}
}
sub bz_column_info {
my ($self, $table, $column) = @_;
- return $self->_bz_real_schema->get_column_abstract($table, $column);
+ my $def = $self->_bz_real_schema->get_column_abstract($table, $column);
+ # We dclone it so callers can't modify the Schema.
+ $def = dclone($def) if defined $def;
+ return $def;
}
sub bz_index_info {
}
}
+# This is used before adding a foreign key to a column, to make sure
+# that the database won't fail adding the key.
+sub _check_references {
+ my ($self, $table, $column, $foreign_table, $foreign_column) = @_;
+
+ my $bad_values = $self->selectcol_arrayref(
+ "SELECT DISTINCT $table.$column
+ FROM $table LEFT JOIN $foreign_table
+ ON $table.$column = $foreign_table.$foreign_column
+ WHERE $foreign_table.$foreign_column IS NULL
+ AND $table.$column IS NOT NULL");
+
+ if (@$bad_values) {
+ my $values = join(', ', @$bad_values);
+ print <<EOT;
+
+ERROR: There are invalid values for the $column column in the $table
+table. (These values do not exist in the $foreign_table table, in the
+$foreign_column column.)
+
+Before continuing with checksetup, you will need to fix these values,
+either by deleting these rows from the database, or changing the values
+of $column in $table to point to valid values in $foreign_table.$foreign_column.
+
+The bad values from the $table.$column column are:
+$values
+
+EOT
+ # I just picked a number above 2, to be considered "abnormal exit."
+ exit 3;
+ }
+}
+
1;
__END__
use Bugzilla::Util;
use Bugzilla::Constants;
+use Carp qw(confess);
use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys);
use Safe;
# Historical, needed for SCHEMA_VERSION = '1.00'
=over
-=item C<TABLES_FIRST>
-
-Because of L</"Referential Integrity">, certain tables must be created
-before other tables. L</ABSTRACT_SCHEMA> can't specify this, because
-it's a hash. So we specify it here. When calling C<get_table_list()>,
-these tables will be returned before the other tables.
-
-=cut
-
-use constant TABLES_FIRST => qw(
- fielddefs
- profiles
-);
-
=item C<SCHEMA_VERSION>
The 'version' of the internal schema structure. This version number
=item C<DELETE>
What to do if the row in the parent table is deleted. Choices are
-C<RESTRICT> or C<CASCADE>.
+C<RESTRICT>, C<CASCADE>, or C<SET NULL>.
C<RESTRICT> means the deletion of the row in the parent table will
be forbidden by the database if there is a row in I<this> table that
C<CASCADE> means that this row will be deleted along with that row.
+C<SET NULL> means that the column will be set to C<NULL> when the parent
+row is deleted. Note that this is only valid if the column can actually
+be set to C<NULL>. (That is, the column isn't C<NOT NULL>.)
+
=item C<UPDATE>
-What to do if the value in the parent table is updated. It has the
-same choices as L</DELETE>, except it defaults to C<CASCADE>, which means
-"also update this column in this table."
+What to do if the value in the parent table is updated. You can set this
+to C<CASCADE> or C<RESTRICT>, which mean the same thing as they do for
+L</DELETE>. This variable defaults to C<CASCADE>, which means "also
+update this column in this table."
=back
-Note that not all our supported databases actually enforce C<RESTRICT>
-and C<CASCADE>, so don't depend on them.
-
=cut
use constant SCHEMA_VERSION => '2.00';
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
attach_id => {TYPE => 'INT3'},
- who => {TYPE => 'INT3', NOTNULL => 1},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid'}},
bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
fieldid => {TYPE => 'INT3', NOTNULL => 1},
added => {TYPE => 'TINYTEXT'},
cc => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
- who => {TYPE => 'INT3', NOTNULL => 1},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
],
INDEXES => [
cc_bug_id_idx => {FIELDS => [qw(bug_id who)],
votes => {
FIELDS => [
- who => {TYPE => 'INT3', NOTNULL => 1},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
bug_id => {TYPE => 'INT3', NOTNULL => 1},
vote_count => {TYPE => 'INT2', NOTNULL => 1},
],
mimetype => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
ispatch => {TYPE => 'BOOLEAN'},
filename => {TYPE => 'varchar(100)', NOTNULL => 1},
- submitter_id => {TYPE => 'INT3', 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,
email_setting => {
FIELDS => [
- user_id => {TYPE => 'INT3', NOTNULL => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
relationship => {TYPE => 'INT1', NOTNULL => 1},
event => {TYPE => 'INT1', NOTNULL => 1},
],
watch => {
FIELDS => [
- watcher => {TYPE => 'INT3', NOTNULL => 1},
- watched => {TYPE => 'INT3', NOTNULL => 1},
+ 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)],
FIELDS => [
id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
- userid => {TYPE => 'INT3', NOTNULL => 1},
+ userid => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
query_type => {TYPE => 'BOOLEAN', NOTNULL => 1},
namedqueries_link_in_footer => {
FIELDS => [
- namedquery_id => {TYPE => 'INT3', NOTNULL => 1},
- user_id => {TYPE => 'INT3', NOTNULL => 1},
+ 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)],
component_cc => {
FIELDS => [
- user_id => {TYPE => 'INT3', NOTNULL => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
component_id => {TYPE => 'INT2', NOTNULL => 1},
],
INDEXES => [
FIELDS => [
cookie => {TYPE => 'varchar(16)', NOTNULL => 1,
PRIMARYKEY => 1},
- userid => {TYPE => 'INT3', NOTNULL => 1},
+ userid => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
ipaddr => {TYPE => 'varchar(40)', NOTNULL => 1},
lastused => {TYPE => 'DATETIME', NOTNULL => 1},
],
# for these changes.
tokens => {
FIELDS => [
- userid => {TYPE => 'INT3'},
+ userid => {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
issuedate => {TYPE => 'DATETIME', NOTNULL => 1} ,
token => {TYPE => 'varchar(16)', NOTNULL => 1,
PRIMARYKEY => 1},
PRIMARYKEY => 1},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1},
- initialowner => {TYPE => 'INT3', NOTNULL => 1},
- initialqacontact => {TYPE => 'INT3'},
+ 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},
],
INDEXES => [
FIELDS => [
id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
NOTNULL => 1},
- eventid => {TYPE => 'INT3', 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,
FIELDS => [
id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
NOTNULL => 1},
- eventid => {TYPE => 'INT3', 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'},
FIELDS => [
id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
NOTNULL => 1},
- owner_userid => {TYPE => 'INT3', NOTNULL => 1},
+ owner_userid => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
subject => {TYPE => 'varchar(128)'},
body => {TYPE => 'MEDIUMTEXT'},
],
profile_setting => {
FIELDS => [
- user_id => {TYPE => 'INT3', NOTNULL => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
setting_name => {TYPE => 'varchar(32)', NOTNULL => 1},
setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
],
my $self = shift;
my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ };
my $type = $finfo->{TYPE};
- die "A valid TYPE was not specified for this column." unless ($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')
} #eosub--get_type_ddl
-# Used if you want to display the DDL to the user, as opposed to actually
-# use it in the database.
-sub get_display_ddl {
- my ($self, $table, $column, $def) = @_;
- my $ddl = $self->get_type_ddl($def);
- if ($def->{REFERENCES}) {
- $ddl .= $self->get_fk_ddl($table, $column, $def->{REFERENCES});
- }
- return $ddl;
-}
-
sub get_fk_ddl {
=item C<_get_fk_ddl>
my $update = $references->{UPDATE} || 'CASCADE';
my $delete = $references->{DELETE} || 'RESTRICT';
- my $to_table = $references->{TABLE} || die "No table in reference";
- my $to_column = $references->{COLUMN} || die "No column in reference";
+ 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"
return "fk_${table}_${column}_${to_table}_${to_column}";
}
-sub _get_add_fk_sql {
- my ($self, $table, $column, $new_def) = @_;
+sub get_add_fk_sql {
+ my ($self, $table, $column, $def) = @_;
- my $fk_string = $self->get_fk_ddl($table, $column, $new_def->{REFERENCES});
+ my $fk_string = $self->get_fk_ddl($table, $column, $def);
return ("ALTER TABLE $table ADD $fk_string");
}
Parameters: none
- Returns: An array of table names. The tables specified
- in L</TABLES_FIRST> will come first, in order. The
- rest of the tables will be in random order.
+ Returns: An array of table names, in alphabetical order.
=cut
my $self = shift;
-
- my %schema = %{$self->{schema}};
- my @tables;
- foreach my $table (TABLES_FIRST) {
- push(@tables, $table);
- delete $schema{$table};
- }
- push(@tables, keys %schema);
- return @tables;
+ return sort keys %{$self->{schema}};
}
sub get_table_columns {
push(@ddl, $index_sql) if $index_sql;
}
- my %fields = @{$self->{schema}{$table}{FIELDS}};
- foreach my $col (keys %fields) {
- my $def = $fields{$col};
- if ($def->{REFERENCES}) {
- push(@ddl, $self->_get_add_fk_sql($table, $col, $def));
- }
- }
-
push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} })
if (ref($self->{schema}{$table}{DB_EXTRAS}));
(push(@statements, "UPDATE $table SET $column = $init_value"))
if defined $init_value;
+ if (defined $definition->{REFERENCES}) {
+ push(@statements, $self->get_add_fk_sql($table, $column,
+ $definition->{REFERENCES}));
+ }
+
return (@statements);
}
push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
}
- # If we went from not having an FK to having an FK
- # XXX For right now, you can't change an FK's actual definition.
- if (!$old_def->{REFERENCES} && $new_def->{REFERENCES}) {
- push(@statements, $self->_get_add_fk_sql($table, $column, $new_def));
- }
- elsif ($old_def->{REFERENCES} && !$new_def->{REFERENCES}) {
- push(@statements, $self->_get_drop_fk_sql($table, $column, $old_def));
- }
-
return @statements;
}
$col_one->{TYPE} = uc($col_one->{TYPE});
$col_two->{TYPE} = uc($col_two->{TYPE});
- # It doesn't work to compare the two REFERENCES items, because
- # they look like 'HASH(0xaf3c434)' to diff_arrays--they'll never
- # be equal.
- my %col_one_def = %$col_one;
- delete $col_one_def{REFERENCES};
- my %col_two_def = %$col_two;
- delete $col_two_def{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_def;
- my @col_two_array = %col_two_def;
+ my @col_one_array = %$col_one;
+ my @col_two_array = %$col_two;
my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array);
# If there are no differences between the arrays, then they are equal.
- my $defs_identical = !scalar(@$removed) && !scalar(@$added);
-
- # If the basic definitions are identical, we still want to check
- # if the two foreign key definitions are different.
- my @fk_array_one = %{$col_one->{REFERENCES} || {}};
- my @fk_array_two = %{$col_two->{REFERENCES} || {}};
- my ($fk_removed, $fk_added) =
- diff_arrays(\@fk_array_one, \@fk_array_two);
- my $fk_identical = !scalar(@$fk_removed) && !scalar(@$fk_added);
-
- return $defs_identical && $fk_identical ? 1 : 0;
+ return !scalar(@$removed) && !scalar(@$added) ? 1 : 0;
}
}
}
-# This is used before adding a foreign key to a column, to make sure
-# that the database won't fail adding the key.
-sub check_references {
- my ($table, $column, $foreign_table, $foreign_column) = @_;
- my $dbh = Bugzilla->dbh;
-
- my $bad_values = $dbh->selectcol_arrayref(
- "SELECT DISTINCT $table.$column
- FROM $table LEFT JOIN $foreign_table
- ON $table.$column = $foreign_table.$foreign_column
- WHERE $foreign_table.$foreign_column IS NULL");
-
- if (@$bad_values) {
- my $values = join(', ', @$bad_values);
- print <<EOT;
-
-ERROR: There are invalid values for the $column column in the $table
-table. (These values do not exist in the $foreign_table table, in the
-$foreign_column column.)
-
-Before continuing with checksetup, you will need to fix these values,
-either by deleting these rows from the database, or changing the values
-of $column in $table to point to valid values in $foreign_table.$foreign_column.
-
-The bad values from the $table.$column column are:
-$values
-
-EOT
- # I just picked a number above 2, to be considered "abnormal exit."
- exit 3;
- }
-}
-
# NOTE: This is NOT the function for general table updates. See
# update_table_definitions for that. This is only for the fielddefs table.
sub update_fielddefs_definition {
if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) {
$dbh->bz_alter_column('components', 'initialqacontact',
{TYPE => 'INT3'});
- $dbh->do("UPDATE components SET initialqacontact = NULL " .
- "WHERE initialqacontact = 0");
}
+ $dbh->do("UPDATE components SET initialqacontact = NULL " .
+ "WHERE initialqacontact = 0");
_migrate_email_prefs_to_new_table();
_initialize_dependency_tree_changes_email_pref();
$dbh->bz_add_column('milestones', 'id',
{TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
- # Referential Integrity begins here
- check_references('profiles_activity', 'userid', 'profiles', 'userid');
- $dbh->bz_alter_column('profiles_activity', 'userid',
- {TYPE => 'INT3', NOTNULL => 1, REFERENCES =>
- {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}});
- check_references('profiles_activity', 'who', 'profiles', 'userid');
- $dbh->bz_alter_column('profiles_activity', 'who',
- {TYPE => 'INT3', NOTNULL => 1, REFERENCES =>
- {TABLE => 'profiles', COLUMN => 'userid'}});
- check_references('profiles_activity', 'fieldid', 'fielddefs', 'id');
- $dbh->bz_alter_column('profiles_activity', 'fieldid',
- {TYPE => 'INT3', NOTNULL => 1, REFERENCES =>
- {TABLE => 'fielddefs', COLUMN => 'id'}});
-
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
Bugzilla::Hook::process('install-update_db');
+
+ $dbh->bz_setup_foreign_keys();
}
# Subroutines should be ordered in the order that they are called.