--- /dev/null
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Andrew Dunstan <andrew@dunslane.net>,
+# Edward J. Sabol <edwardjsabol@iname.com>
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+# Lance Larsh <lance.larsh@oracle.com>
+# Dennis Melentyev <dennis.melentyev@infopulse.com.ua>
+# Akamai Technologies <bugzilla-dev@akamai.com>
+
+package Bugzilla::DB::Schema;
+
+###########################################################################
+#
+# Purpose: Object-oriented, DBMS-independent database schema for Bugzilla
+#
+# This is the base class implementing common methods and abstract schema.
+#
+###########################################################################
+
+use strict;
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Util;
+use Bugzilla::Constants;
+
+use Carp qw(confess);
+use Digest::MD5 qw(md5_hex);
+use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys);
+use Safe;
+# Historical, needed for SCHEMA_VERSION = '1.00'
+use Storable qw(dclone freeze thaw);
+
+# New SCHEMA_VERSION (2.00) use this
+use Data::Dumper;
+
+=head1 NAME
+
+Bugzilla::DB::Schema - Abstract database schema for Bugzilla
+
+=head1 SYNOPSIS
+
+ # Obtain MySQL database schema.
+ # Do not do this. Use Bugzilla::DB instead.
+ use Bugzilla::DB::Schema;
+ my $mysql_schema = new Bugzilla::DB::Schema('Mysql');
+
+ # Recommended way to obtain database schema.
+ use Bugzilla::DB;
+ my $dbh = Bugzilla->dbh;
+ my $schema = $dbh->_bz_schema();
+
+ # Get the list of tables in the Bugzilla database.
+ my @tables = $schema->get_table_list();
+
+ # Get the SQL statements need to create the bugs table.
+ my @statements = $schema->get_table_ddl('bugs');
+
+ # Get the database-specific SQL data type used to implement
+ # the abstract data type INT1.
+ my $db_specific_type = $schema->sql_type('INT1');
+
+=head1 DESCRIPTION
+
+This module implements an object-oriented, abstract database schema.
+It should be considered package-private to the Bugzilla::DB module.
+That means that CGI scripts should never call any function in this
+module directly, but should instead rely on methods provided by
+Bugzilla::DB.
+
+=head1 NEW TO SCHEMA.PM?
+
+If this is your first time looking at Schema.pm, especially if
+you are making changes to the database, please take a look at
+L<http://www.bugzilla.org/docs/developer.html#sql-schema> to learn
+more about how this integrates into the rest of Bugzilla.
+
+=cut
+
+#--------------------------------------------------------------------------
+# Define the Bugzilla abstract database schema and version as constants.
+
+=head1 CONSTANTS
+
+=over
+
+=item C<SCHEMA_VERSION>
+
+The 'version' of the internal schema structure. This version number
+is incremented every time the the fundamental structure of Schema
+internals changes.
+
+This is NOT changed every time a table or a column is added. This
+number is incremented only if the internal structures of this
+Schema would be incompatible with the internal structures of a
+previous Schema version.
+
+In general, unless you are messing around with serialization
+and deserialization of the schema, you don't need to worry about
+this constant.
+
+=begin private
+
+An example of the use of the version number:
+
+Today, we store all individual columns like this:
+
+column_name => { TYPE => 'SOMETYPE', NOTNULL => 1 }
+
+Imagine that someday we decide that NOTNULL => 1 is bad, and we want
+to change it so that the schema instead uses NULL => 0.
+
+But we have a bunch of Bugzilla installations around the world with a
+serialized schema that has NOTNULL in it! When we deserialize that
+structure, it just WILL NOT WORK properly inside of our new Schema object.
+So, immediately after deserializing, we need to go through the hash
+and change all NOTNULLs to NULLs and so on.
+
+We know that we need to do that on deserializing because we know that
+version 1.00 used NOTNULL. Having made the change to NULL, we would now
+be version 1.01.
+
+=end private
+
+=item C<ABSTRACT_SCHEMA>
+
+The abstract database schema structure consists of a hash reference
+in which each key is the name of a table in the Bugzilla database.
+
+The value for each key is a hash reference containing the keys
+C<FIELDS> and C<INDEXES> which in turn point to array references
+containing information on the table's fields and indexes.
+
+A field hash reference should must contain the key C<TYPE>. Optional field
+keys include C<PRIMARYKEY>, C<NOTNULL>, and C<DEFAULT>.
+
+The C<INDEXES> array reference contains index names and information
+regarding the index. If the index name points to an array reference,
+then the index is a regular index and the array contains the indexed
+columns. If the index name points to a hash reference, then the hash
+must contain the key C<FIELDS>. It may also contain the key C<TYPE>,
+which can be used to specify the type of index such as UNIQUE or FULLTEXT.
+
+=back
+
+=head2 Referential Integrity
+
+Bugzilla::DB::Schema supports "foreign keys", a way of saying
+that "Column X may only contain values from Column Y in Table Z".
+For example, in Bugzilla, bugs.resolution should only contain
+values from the resolution.values field.
+
+It does this by adding an additional item to a column, called C<REFERENCES>.
+This is a hash with the following members:
+
+=over
+
+=item C<TABLE>
+
+The table the foreign key points at
+
+=item C<COLUMN>
+
+The column pointed at in that table.
+
+=item C<DELETE>
+
+What to do if the row in the parent table is deleted. Choices are
+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
+still refers to it. This is the default, if you don't specify
+C<DELETE>.
+
+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. 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
+
+=cut
+
+use constant SCHEMA_VERSION => '2.00';
+use constant ADD_COLUMN => 'ADD COLUMN';
+# This is a reasonable default that's true for both PostgreSQL and MySQL.
+use constant MAX_IDENTIFIER_LEN => 63;
+use constant ABSTRACT_SCHEMA => {
+
+ # BUG-RELATED TABLES
+ # ------------------
+
+ # General Bug Information
+ # -----------------------
+ bugs => {
+ FIELDS => [
+ bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ assigned_to => {TYPE => 'INT3', NOTNULL => 1},
+ bug_file_loc => {TYPE => 'MEDIUMTEXT'},
+ 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},
+ rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1},
+ reporter => {TYPE => 'INT3', NOTNULL => 1},
+ version => {TYPE => 'varchar(64)', NOTNULL => 1},
+ component_id => {TYPE => 'INT2', NOTNULL => 1},
+ resolution => {TYPE => 'varchar(64)',
+ NOTNULL => 1, DEFAULT => "''"},
+ target_milestone => {TYPE => 'varchar(20)',
+ NOTNULL => 1, DEFAULT => "'---'"},
+ qa_contact => {TYPE => 'INT3'},
+ status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
+ DEFAULT => "''"},
+ votes => {TYPE => 'INT3', NOTNULL => 1,
+ DEFAULT => '0'},
+ # Note: keywords field is only a cache; the real data
+ # comes from the keywords table
+ keywords => {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(5,2)',
+ NOTNULL => 1, DEFAULT => '0'},
+ remaining_time => {TYPE => 'decimal(5,2)',
+ NOTNULL => 1, DEFAULT => '0'},
+ deadline => {TYPE => 'DATETIME'},
+ alias => {TYPE => 'varchar(20)'},
+ ],
+ INDEXES => [
+ bugs_alias_idx => {FIELDS => ['alias'],
+ TYPE => 'UNIQUE'},
+ 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_votes_idx => ['votes'],
+ ],
+ },
+
+ 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 => [
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ attach_id => {TYPE => 'INT3'},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid'}},
+ bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
+ fieldid => {TYPE => 'INT3', NOTNULL => 1},
+ added => {TYPE => 'TINYTEXT'},
+ removed => {TYPE => 'TINYTEXT'},
+ ],
+ 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'],
+ ],
+ },
+
+ cc => {
+ FIELDS => [
+ bug_id => {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)],
+ TYPE => 'UNIQUE'},
+ cc_who_idx => ['who'],
+ ],
+ },
+
+ longdescs => {
+ FIELDS => [
+ comment_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ who => {TYPE => 'INT3', NOTNULL => 1},
+ bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
+ work_time => {TYPE => 'decimal(5,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 => ['bug_id'],
+ longdescs_who_idx => [qw(who bug_id)],
+ longdescs_bug_when_idx => ['bug_when'],
+ ],
+ },
+
+ dependencies => {
+ FIELDS => [
+ blocked => {TYPE => 'INT3', NOTNULL => 1},
+ dependson => {TYPE => 'INT3', NOTNULL => 1},
+ ],
+ INDEXES => [
+ dependencies_blocked_idx => ['blocked'],
+ dependencies_dependson_idx => ['dependson'],
+ ],
+ },
+
+ votes => {
+ FIELDS => [
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ vote_count => {TYPE => 'INT2', NOTNULL => 1},
+ ],
+ INDEXES => [
+ votes_who_idx => ['who'],
+ votes_bug_id_idx => ['bug_id'],
+ ],
+ },
+
+ attachments => {
+ FIELDS => [
+ attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ 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'},
+ filename => {TYPE => 'varchar(100)', 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'},
+ isurl => {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},
+ thedata => {TYPE => 'LONGBLOB', NOTNULL => 1},
+ ],
+ },
+
+ duplicates => {
+ FIELDS => [
+ dupe_of => {TYPE => 'INT3', NOTNULL => 1},
+ dupe => {TYPE => 'INT3', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ ],
+ },
+
+ # Keywords
+ # --------
+
+ keyworddefs => {
+ FIELDS => [
+ id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ name => {TYPE => 'varchar(64)', NOTNULL => 1},
+ description => {TYPE => 'MEDIUMTEXT'},
+ ],
+ INDEXES => [
+ keyworddefs_name_idx => {FIELDS => ['name'],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
+ keywords => {
+ FIELDS => [
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ keywordid => {TYPE => 'INT2', NOTNULL => 1},
+ ],
+ 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},
+ status => {TYPE => 'char(1)', NOTNULL => 1},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ attach_id => {TYPE => 'INT3'},
+ creation_date => {TYPE => 'DATETIME', NOTNULL => 1},
+ modification_date => {TYPE => 'DATETIME'},
+ setter_id => {TYPE => 'INT3'},
+ requestee_id => {TYPE => 'INT3'},
+ ],
+ 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'},
+ request_group_id => {TYPE => 'INT3'},
+ ],
+ },
+
+ # "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},
+ product_id => {TYPE => 'INT2'},
+ component_id => {TYPE => 'INT2'},
+ ],
+ INDEXES => [
+ flaginclusions_type_id_idx =>
+ [qw(type_id product_id component_id)],
+ ],
+ },
+
+ flagexclusions => {
+ FIELDS => [
+ type_id => {TYPE => 'INT2', NOTNULL => 1},
+ product_id => {TYPE => 'INT2'},
+ component_id => {TYPE => 'INT2'},
+ ],
+ INDEXES => [
+ flagexclusions_type_id_idx =>
+ [qw(type_id product_id component_id)],
+ ],
+ },
+
+ # 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},
+ 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'},
+ ],
+ INDEXES => [
+ fielddefs_name_idx => {FIELDS => ['name'],
+ TYPE => 'UNIQUE'},
+ fielddefs_sortkey_idx => ['sortkey'],
+ ],
+ },
+
+ # 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},
+ ],
+ 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},
+ value => {TYPE => 'varchar(20)', NOTNULL => 1},
+ sortkey => {TYPE => 'INT2', NOTNULL => 1,
+ DEFAULT => 0},
+ ],
+ INDEXES => [
+ milestones_product_id_idx => {FIELDS => [qw(product_id value)],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
+ # Global Field Values
+ # -------------------
+
+ bug_status => {
+ 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'},
+ is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
+ ],
+ INDEXES => [
+ bug_status_value_idx => {FIELDS => ['value'],
+ TYPE => 'UNIQUE'},
+ bug_status_sortkey_idx => ['sortkey', 'value'],
+ ],
+ },
+
+ resolution => {
+ 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'},
+ ],
+ INDEXES => [
+ resolution_value_idx => {FIELDS => ['value'],
+ TYPE => 'UNIQUE'},
+ resolution_sortkey_idx => ['sortkey', 'value'],
+ ],
+ },
+
+ bug_severity => {
+ 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'},
+ ],
+ INDEXES => [
+ bug_severity_value_idx => {FIELDS => ['value'],
+ TYPE => 'UNIQUE'},
+ bug_severity_sortkey_idx => ['sortkey', 'value'],
+ ],
+ },
+
+ priority => {
+ 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'},
+ ],
+ INDEXES => [
+ priority_value_idx => {FIELDS => ['value'],
+ TYPE => 'UNIQUE'},
+ priority_sortkey_idx => ['sortkey', 'value'],
+ ],
+ },
+
+ rep_platform => {
+ 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'},
+ ],
+ INDEXES => [
+ rep_platform_value_idx => {FIELDS => ['value'],
+ TYPE => 'UNIQUE'},
+ rep_platform_sortkey_idx => ['sortkey', 'value'],
+ ],
+ },
+
+ op_sys => {
+ 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'},
+ ],
+ INDEXES => [
+ op_sys_value_idx => {FIELDS => ['value'],
+ TYPE => 'UNIQUE'},
+ op_sys_sortkey_idx => ['sortkey', 'value'],
+ ],
+ },
+
+ status_workflow => {
+ FIELDS => [
+ # On bug creation, there is no old value.
+ old_status => {TYPE => 'INT2'},
+ new_status => {TYPE => 'INT2', NOTNULL => 1},
+ 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)'},
+ ],
+ INDEXES => [
+ profiles_login_name_idx => {FIELDS => ['login_name'],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
+ profiles_activity => {
+ FIELDS => [
+ 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'},
+ ],
+ },
+
+ 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},
+ query_type => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0},
+ ],
+ 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'],
+ ],
+ },
+
+ component_cc => {
+
+ FIELDS => [
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ component_id => {TYPE => 'INT2', NOTNULL => 1},
+ ],
+ 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)', NOTNULL => 1},
+ lastused => {TYPE => 'DATETIME', NOTNULL => 1},
+ ],
+ INDEXES => [
+ logincookies_lastused_idx => ['lastused'],
+ ],
+ },
+
+ # "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(8)', 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},
+ product_id => {TYPE => 'INT3', NOTNULL => 1},
+ entry => {TYPE => 'BOOLEAN', NOTNULL => 1},
+ membercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
+ othercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
+ canedit => {TYPE => 'BOOLEAN', NOTNULL => 1},
+ 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},
+ group_id => {TYPE => 'INT3', NOTNULL => 1},
+ 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},
+ grantor_id => {TYPE => 'INT3', NOTNULL => 1},
+ 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},
+ group_id => {TYPE => 'INT3', NOTNULL => 1},
+ ],
+ 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},
+ group_id => {TYPE => 'INT3', NOTNULL => 1},
+ ],
+ 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},
+ group_id => {TYPE => 'INT3', NOTNULL => 1},
+ ],
+ 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'},
+ description => {TYPE => 'MEDIUMTEXT'},
+ milestoneurl => {TYPE => 'TINYTEXT', NOTNULL => 1,
+ DEFAULT => "''"},
+ disallownew => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 0},
+ votesperuser => {TYPE => 'INT2', NOTNULL => 1,
+ DEFAULT => 0},
+ maxvotesperbug => {TYPE => 'INT2', NOTNULL => 1,
+ DEFAULT => '10000'},
+ votestoconfirm => {TYPE => 'INT2', NOTNULL => 1,
+ DEFAULT => 0},
+ defaultmilestone => {TYPE => 'varchar(20)',
+ NOTNULL => 1, DEFAULT => "'---'"},
+ ],
+ INDEXES => [
+ products_name_idx => {FIELDS => ['name'],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
+ components => {
+ FIELDS => [
+ id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ name => {TYPE => 'varchar(64)', NOTNULL => 1},
+ product_id => {TYPE => 'INT2', NOTNULL => 1},
+ 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 => [
+ 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'},
+ category => {TYPE => 'INT2', NOTNULL => 1},
+ subcategory => {TYPE => 'INT2', NOTNULL => 1},
+ name => {TYPE => 'varchar(64)', NOTNULL => 1},
+ frequency => {TYPE => 'INT2', NOTNULL => 1},
+ last_viewed => {TYPE => 'DATETIME'},
+ query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
+ is_public => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'FALSE'},
+ ],
+ INDEXES => [
+ series_creator_idx =>
+ {FIELDS => [qw(creator category subcategory name)],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
+ series_data => {
+ FIELDS => [
+ series_id => {TYPE => 'INT3', NOTNULL => 1},
+ 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'},
+ ],
+ },
+
+ # QUIPS
+ # -----
+
+ quips => {
+ FIELDS => [
+ quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ userid => {TYPE => 'INT3'},
+ quip => {TYPE => 'MEDIUMTEXT', 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},
+ 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},
+ setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
+ ],
+ INDEXES => [
+ profile_setting_value_unique_idx => {FIELDS => [qw(user_id setting_name)],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
+ # SCHEMA STORAGE
+ # --------------
+
+ bz_schema => {
+ FIELDS => [
+ schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1},
+ version => {TYPE => 'decimal(3,2)', NOTNULL => 1},
+ ],
+ },
+
+};
+
+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'},
+ ],
+ # 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'],
+ ],
+};
+
+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'},
+ ],
+};
+
+
+#--------------------------------------------------------------------------
+
+=head1 METHODS
+
+Note: Methods which can be implemented generically for all DBs are
+implemented in this module. If needed, they can be overridden with
+DB-specific code in a subclass. Methods which are prefixed with C<_>
+are considered protected. Subclasses may override these methods, but
+other modules should not invoke these methods directly.
+
+=cut
+
+#--------------------------------------------------------------------------
+sub new {
+
+=over
+
+=item C<new>
+
+ Description: Public constructor method used to instantiate objects of this
+ class. However, it also can be used as a factory method to
+ instantiate database-specific subclasses when an optional
+ driver argument is supplied.
+ Parameters: $driver (optional) - Used to specify the type of database.
+ This routine C<die>s if no subclass is found for the specified
+ driver.
+ $schema (optional) - A reference to a hash. Callers external
+ to this package should never use this parameter.
+ Returns: new instance of the Schema class or a database-specific subclass
+
+=cut
+
+ 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__);
+
+ my $self = {};
+ bless $self, $class;
+ $self = $self->_initialize(@_);
+
+ return($self);
+
+} #eosub--new
+#--------------------------------------------------------------------------
+sub _initialize {
+
+=item C<_initialize>
+
+ Description: Protected method that initializes an object after
+ instantiation with the abstract schema. All subclasses should
+ override this method. The typical subclass implementation
+ should first call the C<_initialize> method of the superclass,
+ then do any database-specific initialization (especially
+ define the database-specific implementation of the all
+ abstract data types), and then call the C<_adjust_schema>
+ method.
+ Parameters: $abstract_schema (optional) - A reference to a hash. If
+ provided, this hash will be used as the internal
+ representation of the abstract schema instead of our
+ default abstract schema. This is intended for internal
+ use only by deserialize_abstract.
+ Returns: the instance of the Schema class
+
+=cut
+
+ 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);
+
+ # 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;
+
+ return $self;
+
+} #eosub--_initialize
+#--------------------------------------------------------------------------
+sub _adjust_schema {
+
+=item C<_adjust_schema>
+
+ Description: Protected method that alters the abstract schema at
+ instantiation-time to be database-specific. It is a generic
+ enough routine that it can be defined here in the base class.
+ It takes the abstract schema and replaces the abstract data
+ types with database-specific data types.
+ Parameters: none
+ Returns: the instance of the Schema class
+
+=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}};
+ }
+ }
+ }
+
+ return $self;
+
+} #eosub--_adjust_schema
+#--------------------------------------------------------------------------
+sub get_type_ddl {
+
+=item C<get_type_ddl>
+
+=over
+
+=item B<Description>
+
+Public method to convert abstract (database-generic) field specifiers to
+database-specific data types suitable for use in a C<CREATE TABLE> or
+C<ALTER TABLE> SQL statment. If no database-specific field type has been
+defined for the given field type, then it will just return the same field type.
+
+=item B<Parameters>
+
+=over
+
+=item C<$def> - A reference to a hash of a field containing the following keys:
+C<TYPE> (required), C<NOTNULL> (optional), C<DEFAULT> (optional),
+C<PRIMARYKEY> (optional), C<REFERENCES> (optional)
+
+=back
+
+=item B<Returns>
+
+A DDL string suitable for describing a field in a C<CREATE TABLE> or
+C<ALTER TABLE> SQL statement
+
+=back
+
+=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 $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));
+ $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL});
+ $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY});
+
+ return($type_ddl);
+
+} #eosub--get_type_ddl
+
+
+sub get_fk_ddl {
+=item C<_get_fk_ddl>
+
+=over
+
+=item B<Description>
+
+Protected method. Translates the C<REFERENCES> item of a column into SQL.
+
+=item B<Params>
+
+=over
+
+=item C<$table> - The name of the table the reference is from.
+=item C<$column> - The name of the column the reference is from
+=item C<$references> - The C<REFERENCES> hashref from a column.
+
+=back
+
+=item B<Returns>
+
+SQL for to define the foreign key, or an empty string if C<$references>
+is undefined.
+
+=back
+
+=cut
+
+ 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);
+
+ 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}";
+
+ if (length($name) > $self->MAX_IDENTIFIER_LEN) {
+ $name = 'fk_' . $self->_hash_identifier($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);
+}
+
+
+sub get_add_fk_sql {
+ my ($self, $table, $column, $def) = @_;
+
+ my $fk_string = $self->get_fk_ddl($table, $column, $def);
+ return ("ALTER TABLE $table ADD $fk_string");
+}
+
+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");
+}
+
+sub convert_type {
+
+=item C<convert_type>
+
+Converts a TYPE from the L</ABSTRACT_SCHEMA> format into the real SQL type.
+
+=cut
+
+ my ($self, $type) = @_;
+ return $self->{db_specific}->{$type} || $type;
+}
+
+sub get_column {
+=item C<get_column($table, $column)>
+
+ Description: Public method to get the abstract definition of a column.
+ Parameters: $table - the table name
+ $column - a column in the table
+ Returns: a hashref containing information about the column, including its
+ type (C<TYPE>), whether or not it can be null (C<NOTNULL>),
+ its default value if it has one (C<DEFAULT), etc.
+ Returns undef if the table or column does not exist.
+
+=cut
+
+ 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
+
+sub get_table_list {
+
+=item C<get_table_list>
+
+ Description: Public method for discovering what tables should exist in the
+ Bugzilla database.
+
+ Parameters: none
+
+ Returns: An array of table names, in alphabetical order.
+
+=cut
+
+ my $self = shift;
+ return sort keys %{$self->{schema}};
+}
+
+sub get_table_columns {
+
+=item C<get_table_columns>
+
+ Description: Public method for discovering what columns are in a given
+ table in the Bugzilla database.
+ Parameters: $table - the table name
+ Returns: array of column names
+
+=cut
+
+ my($self, $table) = @_;
+ my @ddl = ();
+
+ 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);
+ }
+
+ return @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;
+}
+
+sub get_create_database_sql {
+ my ($self, $name) = @_;
+ return ("CREATE DATABASE $name");
+}
+
+sub get_table_ddl {
+
+=item C<get_table_ddl>
+
+ Description: Public method to generate the SQL statements needed to create
+ the a given table and its indexes in the Bugzilla database.
+ Subclasses may override or extend this method, if needed, but
+ subclasses probably should override C<_get_create_table_ddl>
+ or C<_get_create_index_ddl> instead.
+ Parameters: $table - the table name
+ Returns: an array of strings containing SQL statements
+
+=cut
+
+ my($self, $table) = @_;
+ my @ddl = ();
+
+ 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 @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}));
+
+ return @ddl;
+
+} #eosub--get_table_ddl
+#--------------------------------------------------------------------------
+sub _get_create_table_ddl {
+
+=item C<_get_create_table_ddl>
+
+ Description: Protected method to generate the "create table" SQL statement
+ for a given table.
+ Parameters: $table - the table name
+ Returns: a string containing the DDL statement for the specified table
+
+=cut
+
+ my($self, $table) = @_;
+
+ my $thash = $self->{schema}{$table};
+ die "Table $table does not exist in the database schema."
+ unless (ref($thash));
+
+ my $create_table = "CREATE TABLE $table \(\n";
+
+ my @fields = @{ $thash->{FIELDS} };
+ while (@fields) {
+ my $field = shift(@fields);
+ my $finfo = shift(@fields);
+ $create_table .= "\t$field\t" . $self->get_type_ddl($finfo);
+ $create_table .= "," if (@fields);
+ $create_table .= "\n";
+ }
+
+ $create_table .= "\)";
+
+ return($create_table)
+
+} #eosub--_get_create_table_ddl
+#--------------------------------------------------------------------------
+sub _get_create_index_ddl {
+
+=item C<_get_create_index_ddl>
+
+ Description: Protected method to generate a "create index" SQL statement
+ for a given table and index.
+ Parameters: $table_name - the name of the table
+ $index_name - the name of the index
+ $index_fields - a reference to an array of field names
+ $index_type (optional) - specify type of index (e.g., UNIQUE)
+ Returns: a string containing the DDL statement
+
+=cut
+
+ 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) . "\)";
+
+ return($sql);
+
+} #eosub--_get_create_index_ddl
+#--------------------------------------------------------------------------
+
+sub get_add_column_ddl {
+
+=item C<get_add_column_ddl($table, $column, \%definition, $init_value)>
+
+ Description: Generate SQL to add a column to a table.
+ Params: $table - The table containing the column.
+ $column - The name of the column being added.
+ \%definition - The new definition for the column,
+ in standard C<ABSTRACT_SCHEMA> format.
+ $init_value - (optional) An initial value to set
+ the column to. Should already be SQL-quoted
+ if necessary.
+ Returns: An array of SQL statements.
+
+=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_fk_sql($table, $column,
+ $definition->{REFERENCES}));
+ }
+
+ return (@statements);
+}
+
+sub get_add_index_ddl {
+
+=item C<get_add_index_ddl>
+
+ Description: Gets SQL for creating an index.
+ NOTE: Subclasses should not override this function. Instead,
+ if they need to specify a custom CREATE INDEX statement,
+ they should override C<_get_create_index_ddl>
+ Params: $table - The name of the table the index will be on.
+ $name - The name of the new index.
+ $definition - An index definition. Either a hashref
+ with FIELDS and TYPE or an arrayref
+ containing a list of columns.
+ Returns: An array of SQL statements that will create the
+ requested index.
+
+=cut
+
+ 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);
+}
+
+sub get_alter_column_ddl {
+
+=item C<get_alter_column_ddl($table, $column, \%definition)>
+
+ Description: Generate SQL to alter a column in a table.
+ The column that you are altering must exist,
+ and the table that it lives in must exist.
+ Params: $table - The table containing the column.
+ $column - The name of the column being changed.
+ \%definition - The new definition for the column,
+ in standard C<ABSTRACT_SCHEMA> format.
+ $set_nulls_to - A value to set NULL values to, if
+ your new definition is NOT NULL and contains
+ no DEFAULT, and when there is a possibility
+ that the column could contain NULLs. $set_nulls_to
+ should be already SQL-quoted if necessary.
+ Returns: An array of SQL statements.
+
+=cut
+
+ 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};
+ # 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) )
+ {
+ $default = $specific->{$default} if exists $specific->{$default};
+ 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}) {
+ my $setdefault;
+ # Handle any fields that were NULL before, if we have a default,
+ $setdefault = $new_def->{DEFAULT} if exists $new_def->{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 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;
+}
+
+sub get_drop_index_ddl {
+
+=item C<get_drop_index_ddl($table, $name)>
+
+ Description: Generates SQL statements to drop an index.
+ Params: $table - The table the index is on.
+ $name - The name of the index being dropped.
+ Returns: An array of SQL statements.
+
+=cut
+
+ 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");
+}
+
+sub get_drop_column_ddl {
+
+=item C<get_drop_column_ddl($table, $column)>
+
+ Description: Generate SQL to drop a column from a table.
+ Params: $table - The table containing the column.
+ $column - The name of the column being dropped.
+ Returns: An array of SQL statements.
+
+=cut
+
+ my ($self, $table, $column) = @_;
+ return ("ALTER TABLE $table DROP COLUMN $column");
+}
+
+=item C<get_drop_table_ddl($table)>
+
+ Description: Generate SQL to drop a table from the database.
+ Params: $table - The name of the table to drop.
+ Returns: An array of SQL statements.
+
+=cut
+
+sub get_drop_table_ddl {
+ my ($self, $table) = @_;
+ return ("DROP TABLE $table");
+}
+
+sub get_rename_column_ddl {
+
+=item C<get_rename_column_ddl($table, $old_name, $new_name)>
+
+ Description: Generate SQL to change the name of a column in a table.
+ NOTE: ANSI SQL contains no simple way to rename a column,
+ so this function is ABSTRACT and must be implemented
+ by subclasses.
+ Params: $table - The table containing the column to be renamed.
+ $old_name - The name of the column being renamed.
+ $new_name - The name the column is changing to.
+ Returns: An array of SQL statements.
+
+=cut
+
+ die "ANSI SQL has no way to rename a column, and your database driver\n"
+ . " has not implemented a method.";
+}
+
+
+sub get_rename_table_sql {
+
+=item C<get_rename_table_sql>
+
+=over
+
+=item B<Description>
+
+Gets SQL to rename a table in the database.
+
+=item B<Params>
+
+=over
+
+=item C<$old_name> - The current name of the table.
+
+=item C<$new_name> - The new name of the table.
+
+=back
+
+=item B<Returns>: An array of SQL statements to rename a table.
+
+=back
+
+=cut
+
+ my ($self, $old_name, $new_name) = @_;
+ return ("ALTER TABLE $old_name RENAME TO $new_name");
+}
+
+=item C<delete_table($name)>
+
+ Description: Deletes a table from this Schema object.
+ Dies if you try to delete a table that doesn't exist.
+ Params: $name - The name of the table to delete.
+ Returns: nothing
+
+=cut
+
+sub delete_table {
+ my ($self, $name) = @_;
+
+ die "Attempted to delete nonexistent table '$name'." unless
+ $self->get_table_abstract($name);
+
+ delete $self->{abstract_schema}->{$name};
+ delete $self->{schema}->{$name};
+}
+
+sub get_column_abstract {
+
+=item C<get_column_abstract($table, $column)>
+
+ Description: A column definition from the abstract internal schema.
+ cross-database format.
+ Params: $table - The name of the table
+ $column - The name of the column that you want
+ Returns: A hash reference. For the format, see the docs for
+ C<ABSTRACT_SCHEMA>.
+ Returns undef if the column or table does not exist.
+
+=cut
+
+ 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;
+}
+
+=item C<get_indexes_on_column_abstract($table, $column)>
+
+ Description: Gets a list of indexes that are on a given column.
+ Params: $table - The table the column is on.
+ $column - The name of the column.
+ Returns: Indexes in the standard format of an INDEX
+ entry on a table. That is, key-value pairs
+ where the key is the index name and the value
+ is the index definition.
+ If there are no indexes on that column, we return
+ undef.
+
+=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});
+ }
+ }
+ }
+
+ return %ret_hash;
+}
+
+sub get_index_abstract {
+
+=item C<get_index_abstract($table, $index)>
+
+ Description: Returns an index definition from the internal abstract schema.
+ Params: $table - The table the index is on.
+ $index - The name of the index.
+ Returns: A hash reference representing an index definition.
+ See the C<ABSTRACT_SCHEMA> docs for details.
+ Returns undef if the index does not exist.
+
+=cut
+
+ 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;
+}
+
+=item C<get_table_abstract($table)>
+
+ Description: Gets the abstract definition for a table in this Schema
+ object.
+ Params: $table - The name of the table you want a definition for.
+ Returns: An abstract table definition, or undef if the table doesn't
+ exist.
+
+=cut
+
+sub get_table_abstract {
+ my ($self, $table) = @_;
+ return $self->{abstract_schema}->{$table};
+}
+
+=item C<add_table($name, \%definition)>
+
+ Description: Creates a new table in this Schema object.
+ If you do not specify a definition, we will
+ simply create an empty table.
+ Params: $name - The name for the new table.
+ \%definition (optional) - An abstract definition for
+ the new table.
+ Returns: nothing
+
+=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 => []};
+ }
+}
+
+
+
+sub rename_table {
+
+=item C<rename_table>
+
+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);
+}
+
+sub delete_column {
+
+=item C<delete_column($table, $column)>
+
+ Description: Deletes a column from this Schema object.
+ Params: $table - Name of the table that the column is in.
+ The table must exist, or we will fail.
+ $column - Name of the column to delete.
+ Returns: nothing
+
+=cut
+
+ my ($self, $table, $column) = @_;
+
+ my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS};
+ my $name_position = lsearch($abstract_fields, $column);
+ 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);
+
+ $self->{schema} = dclone($self->{abstract_schema});
+ $self->_adjust_schema();
+}
+
+sub rename_column {
+
+=item C<rename_column($table, $old_name, $new_name)>
+
+ Description: Renames a column on a table in the Schema object.
+ The column that you are renaming must exist.
+ Params: $table - The table the column is on.
+ $old_name - The current name of the column.
+ $new_name - The new name of hte column.
+ Returns: nothing
+
+=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);
+}
+
+sub set_column {
+
+=item C<set_column($table, $column, \%new_def)>
+
+ Description: Changes the definition of a column in this Schema object.
+ If the column doesn't exist, it will be added.
+ The table that you specify must already exist in the Schema.
+ NOTE: This does not affect the database on the disk.
+ Use the C<Bugzilla::DB> "Schema Modification Methods"
+ if you want to do that.
+ Params: $table - The name of the table that the column is on.
+ $column - The name of the column.
+ \%new_def - The new definition for the column, in
+ C<ABSTRACT_SCHEMA> format.
+ Returns: nothing
+
+=cut
+
+ my ($self, $table, $column, $new_def) = @_;
+
+ my $fields = $self->{abstract_schema}{$table}{FIELDS};
+ $self->_set_object($table, $column, $new_def, $fields);
+}
+
+sub set_index {
+
+=item C<set_index($table, $name, $definition)>
+
+ Description: Changes the definition of an index in this Schema object.
+ If the index doesn't exist, it will be added.
+ The table that you specify must already exist in the Schema.
+ NOTE: This does not affect the database on the disk.
+ Use the C<Bugzilla::DB> "Schema Modification Methods"
+ if you want to do that.
+ Params: $table - The table the index is on.
+ $name - The name of the index.
+ $definition - A hashref or an arrayref. An index
+ definition in C<ABSTRACT_SCHEMA> format.
+ Returns: nothing
+
+=cut
+
+ my ($self, $table, $name, $definition) = @_;
+
+ 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);
+}
+
+# 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 $obj_position = lsearch($array_to_change, $name) + 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);
+ }
+
+ $self->{schema} = dclone($self->{abstract_schema});
+ $self->_adjust_schema();
+}
+
+=item C<delete_index($table, $name)>
+
+ Description: Removes an index definition from this Schema object.
+ If the index doesn't exist, we will fail.
+ The table that you specify must exist in the Schema.
+ NOTE: This does not affect the database on the disk.
+ Use the C<Bugzilla::DB> "Schema Modification Methods"
+ if you want to do that.
+ Params: $table - The table the index is on.
+ $name - The name of the index that we're removing.
+ Returns: nothing
+
+=cut
+
+sub delete_index {
+ my ($self, $table, $name) = @_;
+
+ my $indexes = $self->{abstract_schema}{$table}{INDEXES};
+ my $name_position = lsearch($indexes, $name);
+ 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 {
+
+=item C<columns_equal($col_one, $col_two)>
+
+ Description: Tells you if two columns have entirely identical definitions.
+ The TYPE field's value will be compared case-insensitive.
+ However, all other fields will be case-sensitive.
+ Params: $col_one, $col_two - The columns to compare. Hash
+ references, in C<ABSTRACT_SCHEMA> format.
+ Returns: C<1> if the columns are identical, C<0> if they are not.
+
+=back
+
+=cut
+
+ 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});
+
+ # 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 ($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;
+}
+
+
+=head1 SERIALIZATION/DESERIALIZATION
+
+=over 4
+
+=item C<serialize_abstract()>
+
+ Description: Serializes the "abstract" schema into a format
+ that deserialize_abstract() can read in. This is
+ a method, called on a Schema instance.
+ Parameters: none
+ Returns: A scalar containing the serialized, abstract schema.
+ Do not attempt to manipulate this data directly,
+ as the format may change at any time in the future.
+ The only thing you should do with the returned value
+ is either store it somewhere (coupled with appropriate
+ SCHEMA_VERSION) or deserialize it.
+
+=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});
+}
+
+=item C<deserialize_abstract($serialized, $version)>
+
+ Description: Used for when you've read a serialized Schema off the disk,
+ and you want a Schema object that represents that data.
+ Params: $serialized - scalar. The serialized data.
+ $version - A number in the format X.YZ. The "version"
+ of the Schema that did the serialization.
+ See the docs for C<SCHEMA_VERSION> for more details.
+ Returns: A Schema object. It will have the methods of (and work
+ in the same fashion as) the current version of Schema.
+ However, it will represent the serialized data instead of
+ ABSTRACT_SCHEMA.
+=cut
+
+sub deserialize_abstract {
+ my ($class, $serialized, $version) = @_;
+
+ my $thawed_hash;
+ if (int($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')};
+ }
+
+ return $class->new(undef, $thawed_hash);
+}
+
+#####################################################################
+# Class Methods
+#####################################################################
+
+=back
+
+=head1 CLASS METHODS
+
+These methods are generally called on the class instead of on a specific
+object.
+
+=over
+
+=item C<get_empty_schema()>
+
+ Description: Returns a Schema that has no tables. In effect, this
+ Schema is totally "empty."
+ Params: none
+ Returns: A "empty" Schema object.
+
+=back
+
+=cut
+
+sub get_empty_schema {
+ my ($class) = @_;
+ return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
+}
+
+1;
+
+__END__
+
+=head1 ABSTRACT DATA TYPES
+
+The size and range data provided here is only
+intended as a guide. See your database's Bugzilla
+module (in this directory) for the most up-to-date
+values for these data types. The following
+abstract data types are used:
+
+=over 4
+
+=item C<BOOLEAN>
+
+Logical value 0 or 1 where 1 is true, 0 is false.
+
+=item C<INT1>
+
+Integer values (-128 - 127 or 0 - 255 unsigned).
+
+=item C<INT2>
+
+Integer values (-32,768 - 32767 or 0 - 65,535 unsigned).
+
+=item C<INT3>
+
+Integer values (-8,388,608 - 8,388,607 or 0 - 16,777,215 unsigned)
+
+=item C<INT4>
+
+Integer values (-2,147,483,648 - 2,147,483,647 or 0 - 4,294,967,295
+unsigned)
+
+=item C<SMALLSERIAL>
+
+An auto-increment L</INT1>
+
+=item C<MEDIUMSERIAL>
+
+An auto-increment L</INT3>
+
+=item C<INTSERIAL>
+
+An auto-increment L</INT4>
+
+=item C<TINYTEXT>
+
+Variable length string of characters up to 255 (2^8 - 1) characters wide.
+
+=item C<MEDIUMTEXT>
+
+Variable length string of characters up to 4000 characters wide.
+May be longer on some databases.
+
+=item C<LONGTEXT>
+
+Variable length string of characters up to 16M (2^24 - 1) characters wide.
+
+=item C<LONGBLOB>
+
+Variable length string of binary data up to 4M (2^32 - 1) bytes wide
+
+=item C<DATETIME>
+
+DATETIME support varies from database to database, however, it's generally
+safe to say that DATETIME entries support all date/time combinations greater
+than 1900-01-01 00:00:00. Note that the format used is C<YYYY-MM-DD hh:mm:ss>
+to be safe, though it's possible that your database may not require
+leading zeros. For greatest compatibility, however, please make sure dates
+are formatted as above for queries to guarantee consistent results.
+
+=back
+
+Database-specific subclasses should define the implementation for these data
+types as a hash reference stored internally in the schema object as
+C<db_specific>. This is typically done in overridden L<_initialize> method.
+
+The following abstract boolean values should also be defined on a
+database-specific basis:
+
+=over 4
+
+=item C<TRUE>
+
+=item C<FALSE>
+
+=back
+
+=head1 SEE ALSO
+
+L<Bugzilla::DB>
+
+L<http://www.bugzilla.org/docs/developer.html#sql-schema>
+
+=cut