File:  [LON-CAPA] / loncom / interface / lonmysql.pm
Revision 1.7: download - view: text, annotated - select for diffs
Wed Aug 21 21:29:51 2002 UTC (21 years, 8 months ago) by matthew
Branches: MAIN
CVS tags: version_0_6_2, version_0_6, HEAD
Deal cleanly with $dbh not being defined at all.  I have no idea how this
happens, but appearantly it does.

# The LearningOnline Network with CAPA
# MySQL utility functions
#
# $Id: lonmysql.pm,v 1.7 2002/08/21 21:29:51 matthew Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#
######################################################################

package Apache::lonmysql;

use strict;
use DBI;
use Apache::lonnet();

######################################################################
######################################################################

=pod 

=head1 Name

lonmysql - LONCAPA MySQL utility functions

=head1 Synopsis

lonmysql contains utility functions to make accessing the mysql loncapa
database easier.  

=head1 Description

lonmysql does its best to encapsulate all the database/table functions
and provide a common interface.  The goal, however, is not to provide 
a complete reimplementation of the DBI interface.  Instead we try to 
make using mysql as painless as possible.

Each table has a numeric ID that is a parameter to most lonmysql functions.
The table id is returned by &create_table.  
If you lose the table id, it is lost forever.
The table names in MySQL correspond to 
$ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.$table_id.  If the table id 
is non-numeric, it is assumed to be the full name of a table.  If you pass
the table id in a form, you MUST ensure that what you send to lonmysql is
numeric, otherwise you are opening up all the tables in the MySQL database.

=over 4

=item Creating a table

To create a table, you need a description of its structure.  See the entry
for &create_table for a description of what is needed.

 $table_id = &create_table({ 
            columns => {
                id => {
                    type => 'INT',
                    restrictions => 'NOT NULL',
                    primary_key => 'yes',
                    auto_inc    => 'yes'
                    }
                verbage => { type => 'TEXT' },
            },
            column_order => [qw/id verbage idx_verbage/],
            fulltext => [qw/verbage/],
        });

The above command will create a table with two columns, 'id' and 'verbage'.

'id' will be an integer which is autoincremented and non-null.

'verbage' will be of type 'TEXT', which (conceivably) allows any length
text string to be stored.  Depending on your intentions for this database,
setting restrictions => 'NOT NULL' may help you avoid storing empty data.

the fulltext element sets up the 'verbage' column for 'FULLTEXT' searching.



=item Storing rows

Storing a row in a table requires calling &store_row($table_id,$data)

$data is either a hash reference or an array reference.  If it is an array
reference, the data is passed as is (after being escaped) to the 
"INSERT INTO <table> VALUES( ... )" SQL command.  If $data is a hash reference,
the data will be placed into an array in the proper column order for the table
and then passed to the database.

An example of inserting into the table created above is:

&store_row($table_id,[undef,'I am not a crackpot!']);

or equivalently,

&store_row($table_id,{ verbage => 'I am not a crackpot!'});

Since the above table was created with the first column ('id') as 
autoincrement, providing a value is unnecessary even though the column was
marked as 'NOT NULL'.



=item Retrieving rows

Retrieving rows requires calling get_rows:

@row = &Apache::lonmysql::get_rows($table_id,$condition)

This results in the query "SELECT * FROM <table> HAVING $condition".

@row = &Apache::lonmysql::get_rows($table_id,'id>20'); 

returns all rows with column 'id' greater than 20.

=back

=cut

######################################################################
######################################################################
=pod

=head1 Package Variables

=over 4

=cut

##################################################
##################################################

=pod

=item %Tables

Holds information regarding the currently open connections.  Each key
in the %Tables hash will be a unique table key.  The value associated 
with a key is a hash reference.  Most values are initialized when the 
table is created.

The following entries are allowed in the hash reference:

=over 4

=item Name

Table name.

=item Type            

The type of table, typically MyISAM.

=item Row_format

Describes how rows should be stored in the table.  DYNAMIC or STATIC.

=item Create_time

The date of the tables creation.

=item Update_time

The date of the last modification of the table.

=item Check_time

Usually NULL. 

=item Avg_row_length

The average length of the rows.

=item Data_length

The length of the data stored in the table (bytes)

=item Max_data_length

The maximum possible size of the table (bytes).

=item Index_length

The length of the index for the table (bytes)

=item Data_free

I have no idea what this is.

=item Comment 

The comment associated with the table.

=item Rows

The number of rows in the table.

=item Auto_increment

The value of the next auto_increment field.

=item Create_options

I have no idea.

=item Col_order

an array reference which holds the order of columns in the table.

=item row_insert_sth 

The statement handler for row inserts.

=back

Col_order and row_insert_sth are kept internally by lonmysql and are not
part of the usual MySQL table information.

=cut

##################################################
##################################################
my %Tables;

##################################################
##################################################
=pod

=item $errorstring

Holds the last error.

=cut
##################################################
##################################################
my $errorstring;

##################################################
##################################################
=pod

=item $debugstring

Describes current events within the package.

=cut
##################################################
##################################################
my $debugstring;

##################################################
##################################################

=pod

=item $dbh

The database handler; The actual connection to MySQL via the perl DBI.

=cut

##################################################
##################################################
my $dbh;

##################################################
##################################################

# End of global variable declarations

=pod

=back

=cut

######################################################################
######################################################################

=pod

=head1 Internals

=over 4

=cut

######################################################################
######################################################################

=pod

=item &connect_to_db()

Inputs: none.  

Returns: undef on error, 1 on success.

Checks to make sure the database has been connected to.  If not, the
connection is established.  

=cut

###############################
sub connect_to_db { 
    return 1 if ($dbh);
    if (! ($dbh = DBI->connect("DBI:mysql:loncapa","www",
                               $Apache::lonnet::perlvar{'lonSqlAccess'},
                               { RaiseError=>0,PrintError=>0}))) {
        $debugstring = "Unable to connect to loncapa database.";    
        if (! defined($dbh)) {
            $debugstring = "Unable to connect to loncapa database.";
            $errorstring = "dbh was undefined.";
        } elsif ($dbh->err) {
            $errorstring = "Connection error: ".$dbh->errstr;
        }
        return undef;
    }
    $debugstring = "Successfully connected to loncapa database.";    
    return 1;
}

###############################

=pod

=item &disconnect_from_db()

Inputs: none.

Returns: Always returns 1.

Severs the connection to the mysql database.

=cut

###############################
sub disconnect_from_db { 
    foreach (keys(%Tables)) {
        # Supposedly, having statement handlers running around after the
        # database connection has been lost will cause trouble.  So we 
        # kill them off just to be sure.
        if (exists($Tables{$_}->{'row_insert_sth'})) {
            delete($Tables{$_}->{'row_insert_sth'});
        }
    }
    $dbh->disconnect if ($dbh);
    $debugstring = "Disconnected from database.";
    $dbh = undef;
    return 1;
}

###############################

=pod

=item &number_of_rows()

Input: table identifier

Returns: the number of rows in the given table, undef on error.

=cut

###############################
sub number_of_rows { 
    my ($table_id) = @_;
    return undef if (! defined(&connect_to_db()));
    return undef if (! defined(&update_table_info($table_id)));
    return $Tables{&translate_id($table_id)}->{'Rows'};
}

###############################

=pod

=item &get_error()

Inputs: none.

Returns: The last error reported.

=cut

###############################
sub get_error {
    return $errorstring;
}

###############################

=pod

=item &get_debug()

Inputs: none.

Returns: A string describing the internal state of the lonmysql package.

=cut

###############################
sub get_debug {
    return $debugstring;
}

###############################

=pod

=item &update_table_info($table_id)

Inputs: table id

Returns: undef on error, 1 on success.

&update_table_info updates the %Tables hash with current information about
the given table.  

The default MySQL table status fields are:

   Name             Type            Row_format
   Max_data_length  Index_length    Data_free
   Create_time      Update_time     Check_time
   Avg_row_length   Data_length     Comment 
   Rows             Auto_increment  Create_options

Additionally, "Col_order" is updated as well.

=cut

###############################
sub update_table_info { 
    my ($table_id) = @_;
    return undef if (! defined(&connect_to_db()));
    my $table_status = &check_table($table_id);
    return undef if (! defined($table_status));
    if (! $table_status) {
        $errorstring = "table $table_id does not exist.";
        return undef;
    }
    my $tablename = &translate_id($table_id);
    #
    # Get MySQLs table status information.
    #
    my @tabledesc = qw/
        Name Type Row_format Rows Avg_row_length Data_length
            Max_data_length Index_length Data_free Auto_increment 
                Create_time Update_time Check_time Create_options Comment /;
    my $db_command = "SHOW TABLE STATUS FROM loncapa LIKE '$tablename'";
    my $sth = $dbh->prepare($db_command);
    $sth->execute();
    if ($sth->err) {
        $errorstring = "$dbh ATTEMPTED:\n".$db_command."\nRESULTING ERROR:\n".
            $sth->errstr;
        &disconnect_from_db();
        return undef;
    }
    #
    my @info=$sth->fetchrow_array;
    for (my $i=0;$i<= $#info ; $i++) {
        $Tables{$tablename}->{$tabledesc[$i]}= $info[$i];
    }
    #
    # Determine the column order
    #
    $db_command = "DESCRIBE $tablename";
    $sth = $dbh->prepare($db_command);
    $sth->execute();
    if ($sth->err) {
        $errorstring = "$dbh ATTEMPTED:\n".$db_command."\nRESULTING ERROR:\n".
            $sth->errstr;
        &disconnect_from_db();
        return undef;
    }
    my $aref=$sth->fetchall_arrayref;
    $Tables{$tablename}->{'Col_order'}=[]; # Clear values.
    # The values we want are the 'Field' entries, the first column.
    for (my $i=0;$i< @$aref ; $i++) {
        push @{$Tables{$tablename}->{'Col_order'}},$aref->[$i]->[0];
    }
    #
    $debugstring = "Retrieved table info for $tablename";
    return 1;
}

###############################

=pod

=item &create_table

Inputs: 
    table description

Input formats:

    table description = {
        permanent  => 'yes' or 'no',
        columns => {
            colA => {
                type         => mysql type,
                restrictions => 'NOT NULL' or empty,
                primary_key  => 'yes' or empty,
                auto_inc     => 'yes' or empty,
            }
            colB  => { .. }
            colZ  => { .. }
        },
        column_order => [ colA, colB, ..., colZ],
    }

Returns:
    undef on error, table id on success.

=cut

###############################
sub create_table {
    return undef if (!defined(&connect_to_db($dbh)));
    my ($table_des)=@_;
    #
    # Build request to create table
    ##################################
    my @Columns;
    my $col_des;
    my $table_id = &get_new_table_id();
    my $tablename = &translate_id($table_id);
    my $request = "CREATE TABLE IF NOT EXISTS ".$tablename." ";
    foreach my $column (@{$table_des->{'column_order'}}) {
        $col_des = '';
        my $coldata = $table_des->{'columns'}->{$column};
        if (lc($coldata->{'type'}) =~ /(enum|set)/) { # 'enum' or 'set'
            $col_des.=$column." ".$coldata->{'type'}."('".
                join("', '",@{$coldata->{'values'}})."')";
        } else {
            $col_des.=$column." ".$coldata->{'type'};
            if (exists($coldata->{'size'})) {
                $col_des.="(".$coldata->{'size'}.")";
            }
        }
        # Modifiers
        if (exists($coldata->{'restrictions'})){
            $col_des.=" ".$coldata->{'restrictions'};
        }
        if (exists($coldata->{'default'})) {
            $col_des.=" DEFAULT '".$coldata->{'default'}."'";
        }
        $col_des.=' AUTO_INCREMENT' if (exists($coldata->{'auto_inc'}) &&
                                        ($coldata->{'auto_inc'} eq 'yes'));
        $col_des.=' PRIMARY KEY'    if (exists($coldata->{'primary_key'}) &&
                                        ($coldata->{'primary_key'} eq 'yes'));
    } continue {
        # skip blank items.
        push (@Columns,$col_des) if ($col_des ne '');
    }
    if (exists($table_des->{'fulltext'}) && (@{$table_des->{'fulltext'}})) {
        push (@Columns,'FULLTEXT ('.join(',',@{$table_des->{'fulltext'}}).')');
    }
    $request .= "(".join(", ",@Columns).") ";
    unless($table_des->{'permanent'} eq 'yes') {
        $request.="COMMENT = 'temporary' ";
    } 
    $request .= "TYPE=MYISAM";
    #
    # Execute the request to create the table
    #############################################
    my $count = $dbh->do($request);
    if (! defined($count)) {
        $errorstring = "$dbh ATTEMPTED:\n".$request."\nRESULTING ERROR:\n";
        return undef;
    }
    #
    # Set up the internal bookkeeping
    #############################################
    delete($Tables{$tablename}) if (exists($Tables{$tablename}));
    return undef if (! defined(&update_table_info($table_id)));
    $debugstring = "Created table $tablename at time ".time.
        " with request\n$request";
    return $table_id;
}

###############################

=pod

=item &get_new_table_id

Used internally to prevent table name collisions.

=cut

###############################
sub get_new_table_id {
    my $newid = 0;
    my @tables = &tables_in_db();
    foreach (@tables) {
        if (/^$ENV{'user.name'}_$ENV{'user.domain'}_(\d+)$/) {
            $newid = $1 if ($1 > $newid);
        }
    }
    return ++$newid;
}

###############################

=pod

=item &get_rows

Inputs: $table_id,$condition

Returns: undef on error, an array ref to (array of) results on success.

Internally, this function does a 'SELECT * FROM table WHERE $condition'.
$condition = 'id>0' will result in all rows where column 'id' has a value
greater than 0 being returned.

=cut

###############################
sub get_rows {
    my ($table_id,$condition) = @_;
    return undef if (! defined(&connect_to_db()));
    my $table_status = &check_table($table_id);
    return undef if (! defined($table_status));
    if (! $table_status) {
        $errorstring = "table $table_id does not exist.";
        return undef;
    }
    my $tablename = &translate_id($table_id);
    my $request = 'SELECT * FROM '.$tablename.' WHERE '.$condition;
    my $sth=$dbh->prepare($request);
    $sth->execute();
    if ($sth->err) {
        $errorstring = "$dbh ATTEMPTED:\n".$request."\nRESULTING ERROR:\n".
            $sth->errstr;
        $debugstring = "Failed to get rows matching $condition";
        return undef;
    }
    $debugstring = "Got rows matching $condition";
    my @Results = @{$sth->fetchall_arrayref};
    return @Results;
}

###############################

=pod

=item &store_row

Inputs: table id, row data

returns undef on error, 1 on success.

=cut

###############################
sub store_row {
    my ($table_id,$rowdata) = @_;
    # 
    return undef if (! defined(&connect_to_db()));
    my $table_status = &check_table($table_id);
    return undef if (! defined($table_status));
    if (! $table_status) {
        $errorstring = "table $table_id does not exist.";
        return undef;
    }
    #
    my $tablename = &translate_id($table_id);
    #
    my $sth;
    if (exists($Tables{$tablename}->{'row_insert_sth'})) {
        $sth = $Tables{$tablename}->{'row_insert_sth'};
    } else {
        # Build the insert statement handler
        return undef if (! defined(&update_table_info($table_id)));
        my $insert_request = 'INSERT INTO '.$tablename.' VALUES(';
        foreach (@{$Tables{$tablename}->{'Col_order'}}) {
            $insert_request.="?,";
        }
        chop $insert_request;
        $insert_request.=")";
        $sth=$dbh->prepare($insert_request);
        $Tables{$tablename}->{'row_insert_sth'}=$sth;
    }
    my @Parameters; 
    if (ref($rowdata) eq 'ARRAY') {
        @Parameters = @$rowdata;
    } elsif (ref($rowdata) eq 'HASH') {
        foreach (@{$Tables{$tablename}->{'Col_order'}}) {
            push(@Parameters,$rowdata->{$_});
        }
    } 
    $sth->execute(@Parameters);
    if ($sth->err) {
        $errorstring = "$dbh ATTEMPTED insert @Parameters RESULTING ERROR:\n".
            $sth->errstr;
        return undef;
    }
    $debugstring = "Stored row.";    
    return 1;
}

###########################################

=pod

=item tables_in_db

Returns a list containing the names of all the tables in the database.
Returns undef on error.

=cut

###########################################
sub tables_in_db {
    return undef if (!defined(&connect_to_db()));
    my $sth=$dbh->prepare('SHOW TABLES');
    $sth->execute();
    if ($sth->err) {
        $errorstring = "$dbh ATTEMPTED:\n".'SHOW TABLES'.
            "\nRESULTING ERROR:\n".$sth->errstr;
        return undef;
    }
    my $aref = $sth->fetchall_arrayref;
    my @table_list=();
    foreach (@$aref) {
        push @table_list,$_->[0];
    }
    $debugstring = "Got list of tables in DB: @table_list";
    return @table_list;
}

###########################################

=pod

=item &translate_id

Used internally to translate a numeric table id into a MySQL table name.
If the input $id contains non-numeric characters it is assumed to have 
already been translated.

Checks are NOT performed to see if the table actually exists.

=cut

###########################################
sub translate_id {
    my $id = shift;
    # id should be a digit.  If it is not a digit we assume the given id
    # is complete and does not need to be translated.
    return $id if ($id =~ /\D/);  
    return $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.$id;
}

###########################################

=pod

=item &check_table($id)

Checks to see if the requested table exists.  Returns 0 (no), 1 (yes), or 
undef (error).

=cut

###########################################
sub check_table {
    my $table_id = shift;
    return undef if (!defined(&connect_to_db()));
    #
    $table_id = &translate_id($table_id);
    my @Table_list = &tables_in_db();
    my $result = 0;
    foreach (@Table_list) {
        if (/^$table_id$/) {
            $result = 1;
            last;
        }
    }
    # If it does not exist, make sure we do not have it listed in %Tables
    delete($Tables{$table_id}) if ((! $result) && exists($Tables{$table_id}));
    $debugstring = "check_table returned $result for $table_id";
    return $result;
}

###########################################

=pod

=item &remove_from_table($table_id,$column,$value)

Executes a "delete from $tableid where $column like binary '$value'".

=cut

###########################################
sub remove_from_table {
    my ($table_id,$column,$value) = @_;
    return undef if (!defined(&connect_to_db()));
    #
    $table_id = &translate_id($table_id);
    my $command = 'DELETE FROM '.$table_id.' WHERE '.$dbh->quote($column).
        " LIKE BINARY ".$dbh->quote($value);
    my $sth = $dbh->prepare($command); 
    $sth->execute();
    if ($sth->err) {
        $errorstring = "ERROR on execution of ".$command."\n".$sth->errstr;
        return undef;
    }
    my $rows = $sth->rows;
    return $rows;
}


1;

__END__;

=pod

=back

=cut

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>