| # -*- 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> |
| # Dennis Melentyev <dennis.melentyev@infopulse.com.ua> |
| |
| 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::Util; |
| |
| 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. |
| |
| =cut |
| |
| #-------------------------------------------------------------------------- |
| # Define the Bugzilla abstract database schema and version as constants. |
| |
| =head1 CONSTANTS |
| |
| =over 4 |
| |
| =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 |
| |
| =cut |
| |
| use constant SCHEMA_VERSION => '2.00'; |
| 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 => 'TEXT'}, |
| 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 => 'MEDIUMTEXT', 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_short_desc_idx => {FIELDS => ['short_desc'], |
| TYPE => 'FULLTEXT'}, |
| ], |
| }, |
| |
| bugs_activity => { |
| FIELDS => [ |
| bug_id => {TYPE => 'INT3', NOTNULL => 1}, |
| attach_id => {TYPE => 'INT3'}, |
| who => {TYPE => 'INT3', NOTNULL => 1}, |
| 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}, |
| ], |
| INDEXES => [ |
| cc_bug_id_idx => {FIELDS => [qw(bug_id who)], |
| TYPE => 'UNIQUE'}, |
| cc_who_idx => ['who'], |
| ], |
| }, |
| |
| longdescs => { |
| FIELDS => [ |
| 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 => 'MEDIUMTEXT'}, |
| isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'FALSE'}, |
| already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'FALSE'}, |
| ], |
| INDEXES => [ |
| longdescs_bug_id_idx => ['bug_id'], |
| longdescs_who_idx => ['who'], |
| longdescs_bug_when_idx => ['bug_when'], |
| longdescs_thetext_idx => {FIELDS => ['thetext'], |
| TYPE => 'FULLTEXT'}, |
| ], |
| }, |
| |
| 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}, |
| 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}, |
| description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, |
| mimetype => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, |
| ispatch => {TYPE => 'BOOLEAN'}, |
| filename => {TYPE => 'varchar(100)', NOTNULL => 1}, |
| thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, |
| submitter_id => {TYPE => 'INT3', NOTNULL => 1}, |
| isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'FALSE'}, |
| isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'FALSE'}, |
| ], |
| INDEXES => [ |
| attachments_bug_id_idx => ['bug_id'], |
| attachments_creation_ts_idx => ['creation_ts'], |
| attachments_submitter_id_idx => ['submitter_id', 'bug_id'], |
| ], |
| }, |
| |
| duplicates => { |
| FIELDS => [ |
| dupe_of => {TYPE => 'INT3', NOTNULL => 1}, |
| dupe => {TYPE => 'INT3', NOTNULL => 1, |
| PRIMARYKEY => 1}, |
| ], |
| }, |
| |
| # Keywords |
| # -------- |
| |
| keyworddefs => { |
| FIELDS => [ |
| id => {TYPE => 'INT2', 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 => 'INT3', 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'}, |
| is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'TRUE'}, |
| ], |
| 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 => 'INT2', NOTNULL => 1, |
| PRIMARYKEY => 1}, |
| name => {TYPE => 'varchar(50)', NOTNULL => 1}, |
| description => {TYPE => 'TEXT'}, |
| 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 => [ |
| fieldid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, |
| PRIMARYKEY => 1}, |
| name => {TYPE => 'varchar(64)', NOTNULL => 1}, |
| description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, |
| mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'FALSE'}, |
| sortkey => {TYPE => 'INT2', NOTNULL => 1}, |
| obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'FALSE'}, |
| ], |
| INDEXES => [ |
| fielddefs_name_idx => {FIELDS => ['name'], |
| TYPE => 'UNIQUE'}, |
| fielddefs_sortkey_idx => ['sortkey'], |
| ], |
| }, |
| |
| # Per-product Field Values |
| # ------------------------ |
| |
| versions => { |
| FIELDS => [ |
| 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 => [ |
| 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'}, |
| ], |
| 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'], |
| ], |
| }, |
| |
| # 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)'}, |
| disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, |
| mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'TRUE'}, |
| refreshed_when => {TYPE => 'DATETIME', NOTNULL => 1}, |
| extern_id => {TYPE => 'varchar(64)'}, |
| ], |
| INDEXES => [ |
| profiles_login_name_idx => {FIELDS => ['login_name'], |
| TYPE => 'UNIQUE'}, |
| ], |
| }, |
| |
| profiles_activity => { |
| FIELDS => [ |
| userid => {TYPE => 'INT3', NOTNULL => 1}, |
| who => {TYPE => 'INT3', NOTNULL => 1}, |
| profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, |
| fieldid => {TYPE => 'INT3', NOTNULL => 1}, |
| 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}, |
| 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}, |
| watched => {TYPE => 'INT3', NOTNULL => 1}, |
| ], |
| INDEXES => [ |
| watch_watcher_idx => {FIELDS => [qw(watcher watched)], |
| TYPE => 'UNIQUE'}, |
| watch_watched_idx => ['watched'], |
| ], |
| }, |
| |
| namedqueries => { |
| FIELDS => [ |
| userid => {TYPE => 'INT3', NOTNULL => 1}, |
| name => {TYPE => 'varchar(64)', NOTNULL => 1}, |
| linkinfooter => {TYPE => 'BOOLEAN', NOTNULL => 1}, |
| query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, |
| ], |
| INDEXES => [ |
| namedqueries_userid_idx => {FIELDS => [qw(userid name)], |
| TYPE => 'UNIQUE'}, |
| ], |
| }, |
| |
| # Authentication |
| # -------------- |
| |
| logincookies => { |
| FIELDS => [ |
| cookie => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, |
| PRIMARYKEY => 1}, |
| userid => {TYPE => 'INT3', NOTNULL => 1}, |
| 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', NOTNULL => 1} , |
| 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 => 'TEXT', NOTNULL => 1}, |
| isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, |
| last_changed => {TYPE => 'DATETIME', NOTNULL => 1}, |
| userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1}, |
| isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, |
| DEFAULT => 'TRUE'}, |
| ], |
| 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}, |
| ], |
| 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 => '0'}, |
| ], |
| 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 => '0'}, |
| ], |
| 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'], |
| ], |
| }, |
| |
| 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'}, |
| ], |
| 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}, |
| disallownew => {TYPE => 'BOOLEAN', NOTNULL => 1}, |
| votesperuser => {TYPE => 'INT2', NOTNULL => 1}, |
| maxvotesperbug => {TYPE => 'INT2', NOTNULL => 1, |
| DEFAULT => '10000'}, |
| votestoconfirm => {TYPE => 'INT2', NOTNULL => 1}, |
| 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}, |
| initialqacontact => {TYPE => 'INT3'}, |
| 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', NOTNULL => 1}, |
| 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}, |
| 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}, |
| 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}, |
| 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}, |
| subject => {TYPE => 'varchar(128)'}, |
| body => {TYPE => 'MEDIUMTEXT'}, |
| ], |
| }, |
| |
| # QUIPS |
| # ----- |
| |
| quips => { |
| FIELDS => [ |
| quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, |
| PRIMARYKEY => 1}, |
| userid => {TYPE => 'INT3'}, |
| quip => {TYPE => 'TEXT', 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'}, |
| ], |
| }, |
| |
| 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}, |
| 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}, |
| ], |
| }, |
| |
| }; |
| #-------------------------------------------------------------------------- |
| |
| =head1 METHODS |
| |
| Note: Methods which can be implemented generically for all DBs are |
| implemented in this module. If needed, they can be overriden 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; |
| |
| $abstract_schema ||= ABSTRACT_SCHEMA; |
| |
| $self->{schema} = dclone($abstract_schema); |
| # While ABSTRACT_SCHEMA cannot be modified, |
| # $self->{abstract_schema} can be. So, we dclone it to prevent |
| # anything from mucking with the constant. |
| $self->{abstract_schema} = dclone($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 defintions 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> |
| |
| 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. |
| Parameters: a hash or 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) |
| Returns: a DDL string suitable for describing a field in a |
| C<CREATE TABLE> or C<ALTER TABLE> SQL statement |
| |
| =cut |
| |
| 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); |
| |
| 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 $fkref = $self->{enable_references} ? $finfo->{REFERENCES} : undef; |
| my $type_ddl = $self->{db_specific}{$type} || $type; |
| $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); |
| $type_ddl .= " DEFAULT $default" if (defined($default)); |
| $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); |
| $type_ddl .= "\n\t\t\t\tREFERENCES $fkref" if $fkref; |
| |
| return($type_ddl); |
| |
| } #eosub--get_type_ddl |
| #-------------------------------------------------------------------------- |
| 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 |
| |
| =cut |
| |
| my $self = shift; |
| |
| return(sort(keys %{ $self->{schema} })); |
| |
| } #eosub--get_table_list |
| #-------------------------------------------------------------------------- |
| 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_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 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 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; |
| |
| 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}; |
| |
| my $typechange = 0; |
| # If the types have changed, we have to deal with that. |
| if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { |
| $typechange = 1; |
| my $type = $new_def->{TYPE}; |
| $type = $specific->{$type} if exists $specific->{$type}; |
| # Make sure we can CAST from the old type to the new without an error. |
| push(@statements, "SELECT CAST($column AS $type) FROM $table LIMIT 1"); |
| # Add a new temporary column of the new type |
| push(@statements, "ALTER TABLE $table ADD COLUMN ${column}_ALTERTEMP" |
| . " $type"); |
| # UPDATE the temp column to have the same values as the old column |
| push(@statements, "UPDATE $table SET ${column}_ALTERTEMP = " |
| . " CAST($column AS $type)"); |
| |
| # Some databases drop a whole index when a column is dropped, |
| # some only remove that column from an index. For consistency, |
| # we manually drop all indexes on the column before dropping the |
| # column. |
| my %col_idx = $self->get_indexes_on_column_abstract($table, $column); |
| foreach my $idx_name (keys %col_idx) { |
| push(@statements, $self->get_drop_index_ddl($table, $idx_name)); |
| } |
| |
| # DROP the old column |
| push(@statements, "ALTER TABLE $table DROP COLUMN $column"); |
| # And rename the temp column to be the new one. |
| push(@statements, "ALTER TABLE $table RENAME COLUMN " |
| . " ${column}_ALTERTEMP TO $column"); |
| |
| # And now, we have to regenerate any indexes that got |
| # dropped, except for the PK index which will be handled |
| # below. |
| foreach my $idx_name (keys %col_idx) { |
| push(@statements, |
| $self->get_add_index_ddl($table, $idx_name, $col_idx{$idx_name})); |
| } |
| } |
| |
| 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, |
| # or we have a default and we changed the data type of the field |
| elsif ( (defined $default && !defined $default_old) || |
| ($default ne $default_old) || |
| ($typechange && defined $new_def->{DEFAULT}) ) { |
| $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 |
| # OR if we changed the type and we are NOT NULL |
| if ( (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) || |
| ($typechange && $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, |
| # or if we changed types and we are a PK. |
| if ( (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) || |
| ($typechange && $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."; |
| } |
| |
| =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 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}); |
| |
| 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 following abstract data types are used: |
| |
| =over 4 |
| |
| =item C<BOOLEAN> |
| |
| =item C<INT1> |
| |
| =item C<INT2> |
| |
| =item C<INT3> |
| |
| =item C<INT4> |
| |
| =item C<SMALLSERIAL> |
| |
| =item C<MEDIUMSERIAL> |
| |
| =item C<INTSERIAL> |
| |
| =item C<TINYTEXT> |
| |
| =item C<MEDIUMTEXT> |
| |
| =item C<TEXT> |
| |
| =item C<LONGBLOB> |
| |
| =item C<DATETIME> |
| |
| =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 overriden 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> |
| |
| =cut |