]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 237862: New database layer for cross-database compatibility
authormkanat%kerio.com <>
Thu, 17 Feb 2005 02:11:06 +0000 (02:11 +0000)
committermkanat%kerio.com <>
Thu, 17 Feb 2005 02:11:06 +0000 (02:11 +0000)
Patch By Tomas Kopal <Tomas.Kopal@altap.cz> r=mkanat, a=myk

Bugzilla/Config.pm
Bugzilla/DB.pm
Bugzilla/DB/Mysql.pm [new file with mode: 0644]
Bugzilla/DB/Pg.pm [new file with mode: 0644]
checksetup.pl
config.cgi
query.cgi

index eae2e75d0d94ba6984a83a6296f4a6b6193e1535..5c070e3728e744868cd91b27c8b568b7c72911f0 100644 (file)
@@ -71,7 +71,7 @@ our $webdotdir = "$datadir/webdot";
 %Bugzilla::Config::EXPORT_TAGS =
   (
    admin => [qw(GetParamList UpdateParams SetParam WriteParams)],
-   db => [qw($db_host $db_port $db_name $db_user $db_pass $db_sock)],
+   db => [qw($db_driver $db_host $db_port $db_name $db_user $db_pass $db_sock)],
    locations => [qw($libpath $localconfig $datadir $templatedir $webdotdir)],
   );
 Exporter::export_ok_tags('admin', 'db', 'locations');
index c76682e2319cceeef501f15d6fe3e5cd1315080b..bfb2f77f35d18017b66d6de99a35d19557cec49d 100644 (file)
@@ -22,6 +22,7 @@
 #                 Jacob Steenhagen <jake@bugzilla.org>
 #                 Bradley Baetz <bbaetz@student.usyd.edu.au>
 #                 Christopher Aillon <christopher@aillon.com>
+#                 Tomas Kopal <Tomas.Kopal@altap.cz>
 
 package Bugzilla::DB;
 
@@ -29,7 +30,11 @@ use strict;
 
 use DBI;
 
-use base qw(Exporter);
+# Inherit the DB class from DBI::db and Exporter
+# Note that we inherit from Exporter just to allow the old, deprecated
+# interface to work. If it gets removed, the Exporter class can be removed
+# from this list.
+use base qw(Exporter DBI::db);
 
 %Bugzilla::DB::EXPORT_TAGS =
   (
@@ -42,6 +47,7 @@ Exporter::export_ok_tags('deprecated');
 
 use Bugzilla::Config qw(:DEFAULT :db);
 use Bugzilla::Util;
+use Bugzilla::Error;
 
 # All this code is backwards compat fu. As such, its a bit ugly. Note the
 # circular dependencies on Bugzilla.pm
@@ -122,43 +128,29 @@ sub PopGlobalSQLState() {
 sub connect_shadow {
     die "Tried to connect to non-existent shadowdb" unless Param('shadowdb');
 
-    my $dsn = "DBI:mysql:host=" . Param("shadowdbhost") .
-      ";database=" . Param('shadowdb') . ";port=" . Param("shadowdbport");
-
-    $dsn .= ";mysql_socket=" . Param("shadowdbsock") if Param('shadowdbsock');
-
-    return _connect($dsn);
+    return _connect($db_driver, Param("shadowdbhost"),
+                    Param('shadowdb'), Param("shadowdbport"),
+                    Param("shadowdbsock"), $db_user, $db_pass);
 }
 
 sub connect_main {
-    my $dsn = "DBI:mysql:host=$db_host;database=$db_name;port=$db_port";
-
-    $dsn .= ";mysql_socket=$db_sock" if $db_sock;
-
-    return _connect($dsn);
+    return _connect($db_driver, $db_host, $db_name, $db_port,
+                    $db_sock, $db_user, $db_pass);
 }
 
 sub _connect {
-    my ($dsn) = @_;
+    my ($driver, $host, $dbname, $port, $sock, $user, $pass) = @_;
 
-    # connect using our known info to the specified db
-    # Apache::DBI will cache this when using mod_perl
-    my $dbh = DBI->connect($dsn,
-                           '',
-                           '',
-                           { RaiseError => 1,
-                             PrintError => 0,
-                             Username => $db_user,
-                             Password => $db_pass,
-                             ShowErrorStatement => 1,
-                             HandleError => \&_handle_error,
-                             TaintIn => 1,
-                             FetchHashKeyName => 'NAME',  
-                             # Note: NAME_lc causes crash on ActiveState Perl
-                             # 5.8.4 (see Bug 253696)
-                             # XXX - This will likely cause problems in DB
-                             # back ends that twiddle column case (Oracle?)
-                           });
+    # DB specific module have the same name as DB driver, here we
+    # just make sure we are not case sensitive
+    (my $db_module = $driver) =~ s/(\w+)/\u\L$1/g;
+    my $pkg_module = "Bugzilla::DB::" . $db_module;
+
+    # do the actual import
+    eval ("require $pkg_module") || die ($@);
+
+    # instantiate the correct DB specific module
+    my $dbh = $pkg_module->new($user, $pass, $host, $dbname, $port, $sock);
 
     return $dbh;
 }
@@ -173,6 +165,39 @@ sub _handle_error {
     return 0; # Now let DBI handle raising the error
 }
 
+# List of abstract methods we are checking the derived class implements
+our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit
+                            sql_to_days sql_date_format sql_interval
+                            bz_lock_tables bz_unlock_tables);
+
+# This overriden import method will check implementation of inherited classes
+# for missing implementation of abstract methods
+# See http://perlmonks.thepen.com/44265.html
+sub import {
+    my $pkg = shift;
+
+    # do not check this module
+    if ($pkg ne __PACKAGE__) {
+        # make sure all abstract methods are implemented
+        foreach my $meth (@_abstract_methods) {
+            $pkg->can($meth)
+                or croak("Class $pkg does not define method $meth");
+        }
+    }
+
+    # Now we want to call our superclass implementation.
+    # If our superclass is Exporter, which is using caller() to find
+    # a namespace to populate, we need to adjust for this extra call.
+    # All this can go when we stop using deprecated functions.
+    my $is_exporter = $pkg->isa('Exporter');
+    $Exporter::ExportLevel++ if $is_exporter;
+    $pkg->SUPER::import(@_);
+    $Exporter::ExportLevel-- if $is_exporter;
+}
+
+# note that when multiple databases are supported, version number does not
+# make sense anymore (as it is DB dependant). This needs to be removed in
+# the future and places where it's used fixed.
 my $cached_server_version;
 sub server_version {
     return $cached_server_version if defined($cached_server_version);
@@ -183,8 +208,8 @@ sub server_version {
     return $cached_server_version;
 }
 
-sub GetFieldDefs {
-    my $dbh = Bugzilla->dbh;
+sub bz_get_field_defs {
+    my ($self) = @_;
 
     my $extra = "";
     if (!&::UserInGroup(Param('timetrackinggroup'))) {
@@ -193,9 +218,9 @@ sub GetFieldDefs {
     }
 
     my @fields;
-    my $sth = $dbh->prepare("SELECT name, description
-                               FROM fielddefs $extra
-                           ORDER BY sortkey");
+    my $sth = $self->prepare("SELECT name, description
+                                FROM fielddefs $extra
+                            ORDER BY sortkey");
     $sth->execute();
     while (my $field_ref = $sth->fetchrow_hashref()) {
         push(@fields, $field_ref);
@@ -203,6 +228,82 @@ sub GetFieldDefs {
     return(@fields);
 }
 
+sub bz_last_key {
+    my ($self, $table, $column) = @_;
+
+    return $self->last_insert_id($db_name, undef, $table, $column);
+}
+
+sub bz_start_transaction {
+    my ($self) = @_;
+
+    if ($self->{private_bz_in_transaction}) {
+        carp("Can't start transaction within another transaction");
+        ThrowCodeError("nested_transaction");
+    } else {
+        # Turn AutoCommit off and start a new transaction
+        $self->begin_work();
+
+        $self->{privateprivate_bz_in_transaction} = 1;
+    }
+}
+
+sub bz_commit_transaction {
+    my ($self) = @_;
+
+    if (!$self->{private_bz_in_transaction}) {
+        carp("Can't commit without a transaction");
+        ThrowCodeError("not_in_transaction");
+    } else {
+        $self->commit();
+
+        $self->{private_bz_in_transaction} = 0;
+    }
+}
+
+sub bz_rollback_transaction {
+    my ($self) = @_;
+
+    if (!$self->{private_bz_in_transaction}) {
+        carp("Can't rollback without a transaction");
+        ThrowCodeError("not_in_transaction");
+    } else {
+        $self->rollback();
+
+        $self->{private_bz_in_transaction} = 0;
+    }
+}
+
+sub db_new {
+    my ($class, $dsn, $user, $pass, $attributes) = @_;
+
+    # set up default attributes used to connect to the database
+    # (if not defined by DB specific implementation)
+    $attributes = { RaiseError => 1,
+                    AutoCommit => 1,
+                    PrintError => 0,
+                    ShowErrorStatement => 1,
+                    HandleError => \&_handle_error,
+                    TaintIn => 1,
+                    FetchHashKeyName => 'NAME',  
+                    # Note: NAME_lc causes crash on ActiveState Perl
+                    # 5.8.4 (see Bug 253696)
+                    # XXX - This will likely cause problems in DB
+                    # back ends that twiddle column case (Oracle?)
+                  } if (!defined($attributes));
+
+    # connect using our known info to the specified db
+    # Apache::DBI will cache this when using mod_perl
+    my $self = DBI->connect($dsn, $user, $pass, $attributes);
+
+    # class variables
+    $self->{private_bz_in_transaction} = 0;
+
+    bless ($self, $class);
+
+    return $self;
+}
+
 1;
 
 __END__
@@ -213,60 +314,249 @@ Bugzilla::DB - Database access routines, using L<DBI>
 
 =head1 SYNOPSIS
 
-  # Connection
-  my $dbh = Bugzilla::DB->connect_main;
-  my $shadow = Bugzilla::DB->connect_shadow;
+  # Obtain db handle
+  use Bugzilla::DB;
+  my $dbh = Bugzilla->dbh;
 
-  # Schema Information
-  my @fields = Bugzilla::DB::GetFieldDefs();
+  # prepare a query using DB methods
+  my $sth = $dbh->prepare("SELECT " .
+                          $dbh->sql_date_format("creation_ts", "%Y%m%d") .
+                          " FROM bugs WHERE bug_status != 'RESOLVED' " .
+                          $dbh->sql_limit(1));
+
+  # Execute the query
+  $sth->execute;
+  
+  # Get the results
+  my @result = $sth->fetchrow_array;
 
-  # Deprecated
-  SendSQL("SELECT COUNT(*) FROM bugs");
-  my $cnt = FetchOneColumn();
+  # Schema Information
+  my @fields = $dbh->bz_get_field_defs();
 
 =head1 DESCRIPTION
 
-This allows creation of a database handle to connect to the Bugzilla database.
-This should never be done directly; all users should use the L<Bugzilla> module
-to access the current C<dbh> instead.
+Functions in this module allows creation of a database handle to connect
+to the Bugzilla database. This should never be done directly; all users
+should use the L<Bugzilla> module to access the current C<dbh> instead.
+
+This module also contains methods extending the returned handle with
+functionality which is different between databases allowing for easy
+customization for particular database via inheritance. These methods
+should be always preffered over hard-coding SQL commands.
 
 Access to the old SendSQL-based database routines are also provided by
 importing the C<:deprecated> tag. These routines should not be used in new
 code.
 
-The only functions that should be used by modern, regular Bugzilla code
-are the "Schema Information" functions.
-
 =head1 CONNECTION
 
 A new database handle to the required database can be created using this
 module. This is normally done by the L<Bugzilla> module, and so these routines
 should not be called from anywhere else.
 
+=head2 Functions
+
 =over 4
 
 =item C<connect_main>
 
-Connects to the main database, returning a new dbh.
+ Description: Function to connect to the main database, returning a new
+              database handle.
+ Params:      none
+ Returns:     new instance of the DB class
 
 =item C<connect_shadow>
 
-Connects to the shadow database, returning a new dbh. This routine C<die>s if
-no shadow database is configured.
+ Description: Function to connect to the shadow database, returning a new
+              database handle.
+              This routine C<die>s if no shadow database is configured.
+ Params:      none
+ Returns:     new instance of the DB class
+
+=item C<_connect>
+
+ Description: Internal function, creates and returns a new, connected
+              instance of the correct DB class.
+              This routine C<die>s if no driver is specified.
+ Params:      $driver = name of the database driver to use
+              $host = host running the database we are connecting to
+              $dbname = name of the database to connect to
+              $port = port the database is listening on
+              $sock = socket the database is listening on
+              $user = username used to log in to the database
+              $pass = password used to log in to the database
+ Returns:     new instance of the DB class
+
+=item C<_handle_error>
+
+ Description: Function passed to the DBI::connect call for error handling.
+              It shortens the error for printing.
+
+=item C<import>
+
+ Description: Overrides the standard import method to check that derived class
+              implements all required abstract methods. Also calls original
+              implementation in its super class.
 
 =back
 
-=head1 SCHEMA INFORMATION
+=head2 Methods
 
-Bugzilla::DB also contains routines to get schema information about the 
-database.
+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.
+Methods which do not have standard implementation are abstract and must
+be implemented for all supported databases separately.
+To avoid confusion with standard DBI methods, all methods returning string with
+formatted SQL command have prefix C<sql_>. All other methods have prefix C<bz_>.
 
 =over 4
 
-=item C<GetFieldDefs>
+=item C<new>
+
+ Description: Constructor
+              Abstract method, should be overriden by database specific code.
+ Params:      $user = username used to log in to the database
+              $pass = password used to log in to the database
+              $host = host running the database we are connecting to
+              $dbname = name of the database to connect to
+              $port = port the database is listening on
+              $sock = socket the database is listening on
+ Returns:     new instance of the DB class
+ Note:        The constructor should create a DSN from the parameters provided and
+              then call C<db_new()> method of its super class to create a new
+              class instance. See C<db_new> description in this module. As per
+              DBI documentation, all class variables must be prefixed with
+              "private_". See L<DBI>.
+
+=item C<sql_regexp>
+
+ Description: Outputs SQL regular expression operator for POSIX regex
+              searches in format suitable for a given database.
+              Abstract method, should be overriden by database specific code.
+ Params:      none
+ Returns:     formatted SQL for regular expression search (e.g. REGEXP)
+              (scalar)
+
+=item C<sql_not_regexp>
+
+ Description: Outputs SQL regular expression operator for negative POSIX
+              regex searches in format suitable for a given database.
+              Abstract method, should be overriden by database specific code.
+ Params:      none
+ Returns:     formatted SQL for negative regular expression search
+              (e.g. NOT REGEXP) (scalar)
+
+=item C<sql_limit>
+
+ Description: Returns SQL syntax for limiting results to some number of rows
+              with optional offset if not starting from the begining.
+              Abstract method, should be overriden by database specific code.
+ Params:      $limit = number of rows to return from query (scalar)
+              $offset = number of rows to skip prior counting (scalar)
+ Returns:     formatted SQL for limiting number of rows returned from query
+              with optional offset (e.g. LIMIT 1, 1) (scalar)
+
+=item C<sql_to_days>
+
+ Description: Outputs SQL syntax for converting date to Julian days.
+              Abstract method, should be overriden by database specific code.
+ Params:      $date = date to convert to days
+ Returns:     formatted SQL for returning date fields in Julian days. (scalar)
+
+=item C<sql_date_format>
+
+ Description: Outputs SQL syntax for formatting dates.
+              Abstract method, should be overriden by database specific code.
+ Params:      $date = date or name of date type column (scalar)
+              $format = format string for date output (scalar)
+              (%Y = year, four digits, %y = year, two digits, %m = month,
+               %d = day, %a = weekday name, 3 letters, %H = hour 00-23,
+               %i = minute, %s = second)
+ Returns:     formatted SQL for date formatting (scalar)
+
+=item C<sql_interval>
+
+ Description: Outputs proper SQL syntax for a time interval function.
+              Abstract method, should be overriden by database specific code.
+ Params:      $interval = the time interval requested (e.g. '30 minutes')
+              (scalar)
+ Returns:     formatted SQL for interval function (scalar)
+
+=item C<bz_lock_tables>
+
+ Description: Performs a table lock operation on specified tables.
+              If the underlying database supports transactions, it should also
+              implicitly start a new transaction.
+              Abstract method, should be overriden by database specific code.
+ Params:      @tables = list of names of tables to lock in MySQL
+              notation (ex. 'bugs AS bugs2 READ', 'logincookies WRITE')
+ Returns:     none
+
+=item C<bz_unlock_tables>
+
+ Description: Performs a table unlock operation
+              If the underlying database supports transactions, it should also
+              implicitly commit or rollback the transaction.
+              Also, this function should allow to be called with the abort flag
+              set even without locking tables first without raising an error
+              to simplify error handling.
+              Abstract method, should be overriden by database specific code.
+ Params:      $abort = true (1) if the operation on locked tables failed
+              (if transactions are supported, the action will be rolled
+              back). False (0) or no param if the operation succeeded.
+ Returns:     none
+
+=item C<bz_last_key>
+
+ Description: Returns the last serial number, usually from a previous INSERT.
+              Must be executed directly following the relevant INSERT.
+              This base implementation uses DBI->last_insert_id. If the
+              DBD supports it, it is the preffered way to obtain the last
+              serial index. If it is not supported, the DB specific code
+              needs to override it with DB specific code.
+ Params:      $table = name of table containing serial column (scalar)
+              $column = name of column containing serial data type (scalar)
+ Returns:     Last inserted ID (scalar)
+
+=item C<bz_get_field_defs>
+
+ Description: Returns a list of all the "bug" fields in Bugzilla. The list
+              contains hashes, with a 'name' key and a 'description' key.
+ Params:      none
+ Returns:     List of all the "bug" fields
+
+=item C<bz_start_transaction>
+
+ Description: Starts a transaction if supported by the database being used
+ Params:      none
+ Returns:     none
+
+=item C<bz_commit_transaction>
+
+ Description: Ends a transaction, commiting all changes, if supported by
+              the database being used
+ Params:      none
+ Returns:     none
+
+=item C<bz_rollback_transaction>
+
+ Description: Ends a transaction, rolling back all changes, if supported by
+              the database being used
+ Params:      none
+ Returns:     none
+
+=item C<db_new>
+
+ Description: Constructor
+ Params:      $dsn = database connection string
+              $user = username used to log in to the database
+              $pass = password used to log in to the database
+              $attributes = set of attributes for DB connection (optional)
+ Returns:     new instance of the DB class
+ Note:        the name of this constructor is not new, as that would make
+              our check for implementation of new() by derived class useles.
 
-Returns a list of all the "bug" fields in Bugzilla. The list contains 
-hashes, with a 'name' key and a 'description' key.
+=back
 
 =head1 DEPRECATED ROUTINES
 
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
new file mode 100644 (file)
index 0000000..d220443
--- /dev/null
@@ -0,0 +1,162 @@
+# -*- 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): Dave Miller <davem00@aol.com>
+#                 Gayathri Swaminath <gayathrik00@aol.com>
+#                 Jeroen Ruigrok van der Werven <asmodai@wxs.nl>
+#                 Dave Lawrence <dkl@redhat.com>
+#                 Tomas Kopal <Tomas.Kopal@altap.cz>
+
+=head1 NAME
+
+Bugzilla::DB::Mysql - Bugzilla database compatibility layer for MySQL
+
+=head1 DESCRIPTION
+
+This module overrides methods of the Bugzilla::DB module with MySQL specific
+implementation. It is instantiated by the Bugzilla::DB module and should never
+be used directly.
+
+For interface details see L<Bugzilla::DB> and L<DBI>.
+
+=cut
+
+package Bugzilla::DB::Mysql;
+
+use strict;
+
+use Bugzilla::Error;
+use Carp;
+
+# This module extends the DB interface via inheritance
+use base qw(Bugzilla::DB);
+
+sub new {
+    my ($class, $user, $pass, $host, $dbname, $port, $sock) = @_;
+
+    # construct the DSN from the parameters we got
+    my $dsn = "DBI:mysql:host=$host;database=$dbname;port=$port";
+    $dsn .= ";mysql_socket=$sock" if $sock;
+    
+    my $self = $class->db_new($dsn, $user, $pass);
+
+    # all class local variables stored in DBI derived class needs to have
+    # a prefix 'private_'. See DBI documentation.
+    $self->{private_bz_tables_locked} = 0;
+
+    bless ($self, $class);
+    
+    return $self;
+}
+
+# when last_insert_id() is supported on MySQL by lowest DBI/DBD version
+# required by Bugzilla, this implementation can be removed.
+sub bz_last_key {
+    my ($self) = @_;
+
+    my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()');
+
+    return $last_insert_id;
+}
+
+sub sql_regexp {
+    return "REGEXP";
+}
+
+sub sql_not_regexp {
+    return "NOT REGEXP";
+}
+
+sub sql_limit {
+    my ($self, $limit,$offset) = @_;
+
+    if (defined($offset)) {
+        return "LIMIT $offset, $limit";
+    } else {
+        return "LIMIT $limit";
+    }
+}
+
+sub sql_to_days {
+    my ($self, $date) = @_;
+
+    return "TO_DAYS($date)";
+}
+
+sub sql_date_format {
+    my ($self, $date, $format) = @_;
+
+    $format = "%Y.%m.%d %H:%i:%s" if !$format;
+    
+    return "DATE_FORMAT($date, " . $self->quote($format) . ")";
+}
+
+sub sql_interval {
+    my ($self, $interval) = @_;
+    
+    return "INTERVAL $interval";
+}
+
+sub bz_lock_tables {
+    my ($self, @tables) = @_;
+
+    # Check first if there was no lock before
+    if ($self->{private_bz_tables_locked}) {
+        carp("Tables already locked");
+        ThrowCodeError("already_locked");
+    } else {
+        $self->do('LOCK TABLE ' . join(', ', @tables)); 
+    
+        $self->{private_bz_tables_locked} = 1;
+    }
+}
+
+sub bz_unlock_tables {
+    my ($self, $abort) = @_;
+    
+    # Check first if there was previous matching lock
+    if (!$self->{private_bz_tables_locked}) {
+        # Abort is allowed even without previous lock for error handling
+        return if $abort;
+        carp("No matching lock");
+        ThrowCodeError("no_matching_lock");
+    } else {
+        $self->do("UNLOCK TABLES");
+    
+        $self->{private_bz_tables_locked} = 0;
+    }
+}
+
+# As Bugzilla currently runs on MyISAM storage, which does not supprt
+# transactions, these functions die when called.
+# Maybe we should just ignore these calls for now, but as we are not
+# using transactions in MySQL yet, this just hints the developers.
+sub bz_start_transaction {
+    die("Attempt to start transaction on DB without transaction support");
+}
+
+sub bz_commit_transaction {
+    die("Attempt to commit transaction on DB without transaction support");
+}
+
+sub bz_rollback_transaction {
+    die("Attempt to rollback transaction on DB without transaction support");
+}
+
+1;
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
new file mode 100644 (file)
index 0000000..a23c386
--- /dev/null
@@ -0,0 +1,180 @@
+# -*- 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): Dave Miller <davem00@aol.com>
+#                 Gayathri Swaminath <gayathrik00@aol.com>
+#                 Jeroen Ruigrok van der Werven <asmodai@wxs.nl>
+#                 Dave Lawrence <dkl@redhat.com>
+#                 Tomas Kopal <Tomas.Kopal@altap.cz>
+
+=head1 NAME
+
+Bugzilla::DB::Pg - Bugzilla database compatibility layer for PostgreSQL
+
+=head1 DESCRIPTION
+
+This module overrides methods of the Bugzilla::DB module with PostgreSQL
+specific implementation. It is instantiated by the Bugzilla::DB module
+and should never be used directly.
+
+For interface details see L<Bugzilla::DB> and L<DBI>.
+
+=cut
+
+package Bugzilla::DB::Pg;
+
+use strict;
+
+use Bugzilla::Error;
+use Carp;
+
+# This module extends the DB interface via inheritance
+use base qw(Bugzilla::DB);
+
+sub new {
+    my ($class, $user, $pass, $host, $dbname, $port) = @_;
+
+    # construct the DSN from the parameters we got
+    my $dsn = "DBI:Pg:host=$host;dbname=$dbname;port=$port";
+    
+    my $self = $class->db_new($dsn, $user, $pass);
+
+    # all class local variables stored in DBI derived class needs to have
+    # a prefix 'private_'. See DBI documentation.
+    $self->{private_bz_tables_locked} = 0;
+
+    bless ($self, $class);
+
+    return $self;
+}
+
+# if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version
+# supported by Bugzilla, this implementation can be removed.
+sub bz_last_key {
+    my ($self, $table, $column) = @_;
+
+    my $seq = $table . "_" . $column . "_seq";
+    my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')");
+
+    return $last_insert_id;
+}
+
+sub sql_regexp {
+    return "~";
+}
+
+sub sql_not_regexp {
+    return "!~" 
+}
+
+sub sql_limit {
+    my ($self, $limit,$offset) = @_;
+
+    if (defined($offset)) {
+        return "LIMIT $limit OFFSET $offset";
+    } else {
+        return "LIMIT $limit";
+    }
+}
+
+sub sql_to_days {
+    my ($self, $date) = @_;
+
+    return "TO_CHAR($date, 'J')::int";
+}
+
+sub sql_date_format {
+    my ($self, $date, $format) = @_;
+    
+    $format = "%Y.%m.%d %H:%i:%s" if !$format;
+
+    $format =~ s/\%Y/YYYY/g;
+    $format =~ s/\%y/YY/g;
+    $format =~ s/\%m/MM/g;
+    $format =~ s/\%d/DD/g;
+    $format =~ s/\%a/Dy/g;
+    $format =~ s/\%H/HH24/g;
+    $format =~ s/\%i/MI/g;
+    $format =~ s/\%s/SS/g;
+
+    return "TO_CHAR($date, " . $self->quote($format) . ")";
+}
+
+sub sql_interval {
+    my ($self, $interval) = @_;
+    
+    return "INTERVAL '$interval'";
+}
+
+sub bz_lock_tables {
+    my ($self, @tables) = @_;
+   
+    # Check first if there was no lock before
+    if ($self->{private_bz_tables_locked}) {
+        carp("Tables already locked");
+        ThrowCodeError("already_locked");
+    } else {
+        my %read_tables;
+        my %write_tables;
+        foreach my $table (@tables) {
+            $table =~ /^([\d\w]+)([\s]+AS[\s]+[\d\w]+)?[\s]+(WRITE|READ)$/i;
+            my $table_name = $1;
+            if ($3 =~ /READ/i) {
+                if (!exists $read_tables{$table_name}) {
+                    $read_tables{$table_name} = undef;
+                }
+            }
+            else {
+                if (!exists $write_tables{$table_name}) {
+                    $write_tables{$table_name} = undef;
+                }
+            }
+        }
+    
+        # Begin Transaction
+        $self->bz_start_transaction();
+        
+        Bugzilla->dbh->do('LOCK TABLE ' . join(', ', keys %read_tables) .
+                          ' IN ROW SHARE MODE') if keys %read_tables;
+        Bugzilla->dbh->do('LOCK TABLE ' . join(', ', keys %write_tables) .
+                          ' IN ROW EXCLUSIVE MODE') if keys %write_tables;
+    }
+}
+
+sub bz_unlock_tables {
+    my ($self, $abort) = @_;
+    
+    # Check first if there was previous matching lock
+    if (!$self->{private_bz_tables_locked}) {
+        # Abort is allowed even without previous lock for error handling
+        return if $abort;
+
+        carp("No matching lock");
+        ThrowCodeError("no_matching_lock");
+    } else {
+        # End transaction, tables will be unlocked automatically
+        if ($abort) {
+            $self->bz_rollback_transaction();
+        } else {
+            $self->bz_commit_transaction();
+        }
+    }
+}
+
+1;
index 19b9e4fd5c843577d06e5efe79b4229a56b0dc9a..953223b6cb17e0d72e430ae13215f4e9be34455b 100755 (executable)
 # The format of that file is....
 #
 # $answer{'db_host'} = '$db_host = "localhost";
+# $db_driver = "mydbdriver";
 # $db_port = 3306;
 # $db_name = "mydbname";
 # $db_user = "mydbuser";';
@@ -644,6 +645,14 @@ END
 
 
 
+LocalVar('db_driver', '
+#
+# What SQL database to use. Default is mysql. List of supported databases
+# can be obtained by listing Bugzilla/DB directory - every module corresponds
+# to one supported database and the name corresponds to a driver name.
+#
+$db_driver = "mysql";
+');
 LocalVar('db_host', '
 #
 # How to access the SQL database:
@@ -793,6 +802,7 @@ if ($newstuff ne "") {
 # Note that we won't need to do this in globals.pl because globals.pl couldn't
 # care less whether they were defined ahead of time or not. 
 my $my_db_check = ${*{$main::{'db_check'}}{SCALAR}};
+my $my_db_driver = ${*{$main::{'db_driver'}}{SCALAR}};
 my $my_db_host = ${*{$main::{'db_host'}}{SCALAR}};
 my $my_db_port = ${*{$main::{'db_port'}}{SCALAR}};
 my $my_db_name = ${*{$main::{'db_name'}}{SCALAR}};
@@ -1505,11 +1515,6 @@ $::ENV{'PATH'} = $origPath;
 # Check if we have access to --MYSQL--
 #
 
-# This settings are not yet changeable, because other code depends on
-# the fact that we use MySQL and not, say, PostgreSQL.
-
-my $db_base = 'mysql';
-
 # No need to "use" this here.  It should already be loaded from the
 # version-checking routines above, and this file won't even compile if
 # DBI isn't installed so the user gets nasty errors instead of our
@@ -1522,15 +1527,15 @@ if ($my_db_check) {
     my $sql_want = "3.23.41";  # minimum version of MySQL
 
 # original DSN line was:
-#    my $dsn = "DBI:$db_base:$my_db_name;$my_db_host;$my_db_port";
+#    my $dsn = "DBI:$my_db_driver:$my_db_name;$my_db_host;$my_db_port";
 # removed the $db_name because we don't know it exists yet, and this will fail
 # if we request it here and it doesn't. - justdave@syndicomm.com 2000/09/16
-    my $dsn = "DBI:$db_base:;$my_db_host;$my_db_port";
+    my $dsn = "DBI:$my_db_driver:;$my_db_host;$my_db_port";
     if ($my_db_sock ne "") {
         $dsn .= ";mysql_socket=$my_db_sock";
     }
     my $dbh = DBI->connect($dsn, $my_db_user, $my_db_pass)
-      or die "Can't connect to the $db_base database. Is the database " .
+      or die "Can't connect to the $my_db_driver database. Is the database " .
         "installed and\nup and running?  Do you have the correct username " .
         "and password selected in\nlocalconfig?\n\n";
     printf("Checking for %15s %-9s ", "MySQL Server", "(v$sql_want)") unless $silent;
@@ -1581,14 +1586,14 @@ EOF
 }
 
 # now get a handle to the database:
-my $connectstring = "dbi:$db_base:$my_db_name:host=$my_db_host:port=$my_db_port";
+my $connectstring = "dbi:$my_db_driver:$my_db_name:host=$my_db_host:port=$my_db_port";
 if ($my_db_sock ne "") {
     $connectstring .= ";mysql_socket=$my_db_sock";
 }
 
 my $dbh = DBI->connect($connectstring, $my_db_user, $my_db_pass)
     or die "Can't connect to the table '$connectstring'.\n",
-           "Have you read the Bugzilla Guide in the doc directory?  Have you read the doc of '$db_base'?\n";
+           "Have you read the Bugzilla Guide in the doc directory?  Have you read the doc of '$my_db_driver'?\n";
 
 END { $dbh->disconnect if $dbh }
 
@@ -2191,7 +2196,7 @@ while (my ($tabname, $fielddef) = each %table) {
     $fielddef =~ s/\$my_platforms/$my_platforms/;
 
     $dbh->do("CREATE TABLE $tabname (\n$fielddef\n) TYPE = MYISAM")
-        or die "Could not create table '$tabname'. Please check your '$db_base' access.\n";
+        or die "Could not create table '$tabname'. Please check your '$my_db_driver' access.\n";
 }
 
 ###########################################################################
index 2d0505db8c250722ca7250209a13fe9849f47992..56751ec0e88b77f65fe8e421d6dd0114eb4cb7cf 100755 (executable)
@@ -83,7 +83,7 @@ $vars->{'open_status'} = \@open_status;
 $vars->{'closed_status'} = \@closed_status;
 
 # Generate a list of fields that can be queried.
-$vars->{'field'} = [Bugzilla::DB::GetFieldDefs()];
+$vars->{'field'} = [Bugzilla->dbh->bz_get_field_defs()];
 
 # Determine how the user would like to receive the output; 
 # default is JavaScript.
index cfe7702dec1e0e78ecdb0132bff27e1dfed1450f..120dca05ebcdbaef973d605a2fe3f315f9d8fc7b 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -351,7 +351,7 @@ $vars->{'bug_severity'} = \@::legal_severity;
 # Boolean charts
 my @fields;
 push(@fields, { name => "noop", description => "---" });
-push(@fields, Bugzilla::DB::GetFieldDefs());
+push(@fields, Bugzilla->dbh->bz_get_field_defs());
 $vars->{'fields'} = \@fields;
 
 # Creating new charts - if the cmd-add value is there, we define the field