############################################################################### # Codestriker: Copyright (c) 2001, 2002 David Sitsky. All rights reserved. # sits@users.sourceforge.net # # This program is free software; you can redistribute it and modify it under # the terms of the GPL. package Codestriker::DB::Database; use strict; use DBI; use Codestriker; use Codestriker::DB::Column; use Codestriker::DB::Index; use Codestriker::DB::PostgreSQL; use Codestriker::DB::MySQL; use Codestriker::DB::Oracle; use Codestriker::DB::ODBC; use Codestriker::DB::SQLite; # Print out creation statements before executing them if this is true. my $_DEBUG = 0; # Base object for all database objects. sub new { my $type = shift; my $self = {}; return bless $self, $type; } # Factory object for retrieving a database object based on what is set # in the configuration. sub get_database { my $type = shift; if ($Codestriker::db =~ /^DBI:mysql/i) { return Codestriker::DB::MySQL->new(); } elsif ($Codestriker::db =~ /^DBI:Pg/i) { return Codestriker::DB::PostgreSQL->new($Codestriker::db); } elsif ($Codestriker::db =~ /^DBI:Odbc/i) { return Codestriker::DB::ODBC->new(); } elsif ($Codestriker::db =~ /^DBI:Oracle/i) { return Codestriker::DB::Oracle->new(); } elsif ($Codestriker::db =~ /^DBI:SQLite/i) { return Codestriker::DB::SQLite->new(); } else { die "Unsupported database type: $Codestriker::db\n"; } } # Create a new database connection with the specified auto_commit and # raise_error properties. If an active connection is already associated # with the database, return that. sub _get_connection { my ($self, $auto_commit, $raise_error) = @_; # If a connection has already been created, return it. return $self->{dbh} if (exists $self->{dbh}); $self->{dbh} = DBI->connect($Codestriker::db, $Codestriker::dbuser, $Codestriker::dbpasswd, {AutoCommit=>$auto_commit, RaiseError=>$raise_error, LongReadLen=>10240000}); # Return the new connection. return $self->{dbh}; } # Release the connection associated with the database, and either commit or # rollback it depending on the value of $commit. sub release_connection { my ($self) = @_; # Check there is an active connection. if (! defined $self->{dbh}) { die "Cannot release connection on database as no active connection\n"; } # Disconnect the connection. $self->{dbh}->disconnect(); $self->{dbh} = undef; } # Create the table in the database for the specified table, and with the # provided type mappings. sub create_table { my ($self, $table) = @_; # Create the initial table entry. my $stmt = "CREATE TABLE " . $table->get_name() . "(\n"; # For each column, add the appropriate statement. my @pk = (); my $first_column = 1; foreach my $column (@{$table->get_columns()}) { push @pk, $column->get_name() if $column->is_primarykey(); # Add the comma for the start of the next field if necessary. if ($first_column) { $first_column = 0; } else { $stmt .= ",\n"; } # Add in the basic field definition. $stmt .= $column->get_name() . " " . $self->_map_type($column->get_type()); # Check if the length constraint is required for a varchar expression. if ($column->get_type() == $Codestriker::DB::Column::TYPE->{VARCHAR}) { $stmt .= "(" . $column->get_length() . ")"; } # Add the "NOT NULL" constraint if the column is mandatory. $stmt .= " NOT NULL" if $column->is_mandatory(); # Add any autoincrement field decorations if required. if ($column->is_autoincrement()) { $stmt .= " " . $self->_get_autoincrement_type(); } } # Now add in the primary definition if required. if (scalar(@pk) > 0) { $stmt .= ",\nPRIMARY KEY (" . (join ', ', @pk) . ")\n"; } # Close off the statement. $stmt .= ")\n"; print STDERR "Statement is: $stmt\n" if $_DEBUG; eval { # Now create the table. $self->{dbh}->do($stmt); # Now create the indexes for this table. foreach my $index (@{$table->get_indexes()}) { my $index_stmt = "CREATE INDEX " . $index->get_name . " ON " . $table->get_name() . "("; $index_stmt .= (join ', ', @{$index->get_column_names()}) . ")"; print STDERR "Index statement is: $index_stmt\n" if $_DEBUG; # Now execute the statement to create the index. $self->{dbh}->do($index_stmt); } # Commit the table creation. $self->commit(); }; if ($@) { eval { $self->rollback() }; die "Unable to create table/indexes.\n"; } } # Method for retrieving the list of current tables attached to the database. # For most DBI implementations, this implementation works fine. sub get_tables() { my $self = shift; # Remove any tables that end in a period, or have backticks. Recent # versions of MySQL are now using backticks around the table name. my @tables = $self->{dbh}->tables; @tables = map { $_ =~ s/.*\.//; $_ } @tables; @tables = map { $_ =~ s/\`//g; $_ } @tables; return @tables; } # Add a field to a specific table. If the field already exists, then catch # the error and continue silently. sub add_field { my ($self, $table, $field, $definition) = @_; my $dbh = $self->{dbh}; my $rc = 0; eval { $dbh->{PrintError} = 0; my $field_type = $self->_map_type($definition); $dbh->do("ALTER TABLE $table ADD COLUMN $field $field_type"); print "Added new field $field to table $table.\n"; $rc = 1; $self->commit(); }; if ($@) { eval { $self->rollback() }; } $dbh->{PrintError} = 1; return $rc; } # Check if the specified column exists in the specified table. sub column_exists { my ($self, $tablename, $columnname) = @_; my $dbh = $self->{dbh}; my $rc = 0; eval { $dbh->{PrintError} = 0; my $stmt = $dbh->prepare_cached("SELECT COUNT($columnname) " . "FROM $tablename"); $rc = defined $stmt && $stmt->execute() ? 1 : 0; $stmt->finish(); $self->commit(); }; if ($@) { eval { $self->rollback() }; } $dbh->{PrintError} = 1; return $rc; } # Method for moving a database table to another name in a safe manner. sub move_table { my ($self, $old_tablename, $new_tablename) = @_; my $dbh = $self->{dbh}; my $rc = 0; eval { $dbh->{PrintError} = 0; my $stmt = $dbh->prepare_cached("ALTER TABLE $old_tablename RENAME TO " . "$new_tablename"); my $rc = defined $stmt && $stmt->execute() ? 1 : 0; $stmt->finish() if (defined $stmt); $self->commit(); }; if ($@) { eval { $self->rollback() }; } $dbh->{PrintError} = 1; return $rc; } # Simple method for committing the current database transaction. sub commit { my ($self) = @_; $self->{dbh}->commit(); } # Simple method for rolling back the current database transaction. sub rollback { my ($self) = @_; $self->{dbh}->rollback(); } 1;