--- loncom/interface/lonmysql.pm 2003/03/21 16:04:42 1.12 +++ loncom/interface/lonmysql.pm 2016/08/14 16:13:22 1.40 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # MySQL utility functions # -# $Id: lonmysql.pm,v 1.12 2003/03/21 16:04:42 matthew Exp $ +# $Id: lonmysql.pm,v 1.40 2016/08/14 16:13:22 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -31,7 +31,40 @@ package Apache::lonmysql; use strict; use DBI; -use Apache::lonnet(); +use POSIX qw(strftime mktime); +use Apache::lonnet; + +my $mysqluser; +my $mysqlpassword; +my $mysqldatabase; +my %db_config; + +sub set_mysql_user_and_password { + # If we are running under Apache and LONCAPA, use the LON-CAPA + # user and password. Otherwise...? ? ? ? + my ($input_mysqluser,$input_mysqlpassword,$input_mysqldatabase) = @_; + if (! defined($mysqldatabase)) { + $mysqldatabase = 'loncapa'; + } + if (defined($input_mysqldatabase)) { + $mysqldatabase = $input_mysqldatabase; + } + if (! defined($mysqluser) || ! defined($mysqlpassword)) { + if (! eval 'require Apache::lonnet();') { + $mysqluser = 'www'; + $mysqlpassword = $Apache::lonnet::perlvar{'lonSqlAccess'}; + } else { + $mysqluser = ''; + $mysqlpassword = ''; + } + } + if (defined($input_mysqluser)) { + $mysqluser = $input_mysqluser; + } + if (defined($input_mysqlpassword)) { + $mysqlpassword = $input_mysqlpassword; + } +} ###################################################################### ###################################################################### @@ -54,14 +87,15 @@ and provide a common interface. The goa 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. +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. (With all +non-word characters removed form user.name and user.domain) 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 @@ -328,8 +362,10 @@ connection is established. ############################### sub connect_to_db { return 1 if ($dbh); - if (! ($dbh = DBI->connect("DBI:mysql:loncapa","www", - $Apache::lonnet::perlvar{'lonSqlAccess'}, + if (! defined($mysqluser) || ! defined($mysqlpassword)) { + &set_mysql_user_and_password(); + } + if (! ($dbh = DBI->connect("DBI:mysql:$mysqldatabase",$mysqluser,$mysqlpassword, { RaiseError=>0,PrintError=>0}))) { $debugstring = "Unable to connect to loncapa database."; if (! defined($dbh)) { @@ -341,6 +377,52 @@ sub connect_to_db { return undef; } $debugstring = "Successfully connected to loncapa database."; + # Determine DB configuration + undef(%db_config); + my $sth = $dbh->prepare("SHOW VARIABLES"); + $sth->execute(); + if ($sth->err()) { + $debugstring = "Unable to retrieve db config variables"; + return undef; + } + foreach my $row (@{$sth->fetchall_arrayref}) { + $db_config{$row->[0]} = $row->[1]; + } + #&Apache::lonnet::logthis("MySQL configuration variables"); + #while (my ($k,$v) = each(%db_config)) { + # &Apache::lonnet::logthis(" '$k' => '$v'"); + #} + # + return 1; +} + +############################### + +=pod + +=item &verify_sql_connection() + +Inputs: none. + +Returns: 0 (failure) or 1 (success) + +Checks to make sure the database can be connected to. It does not +initialize anything in the lonmysql package. + +=cut + +############################### +sub verify_sql_connection { + if (! defined($mysqluser) || ! defined($mysqlpassword)) { + &set_mysql_user_and_password(); + } + my $connection; + if (! ($connection = DBI->connect("DBI:mysql:loncapa", + $mysqluser,$mysqlpassword, + { RaiseError=>0,PrintError=>0}))) { + return 0; + } + undef($connection); return 1; } @@ -490,10 +572,6 @@ sub update_table_info { # # 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(); @@ -503,10 +581,16 @@ sub update_table_info { &disconnect_from_db(); return undef; } + my @column_name = @{$sth->{NAME}}; # my @info=$sth->fetchrow_array; for (my $i=0;$i<= $#info ; $i++) { - $Tables{$tablename}->{$tabledesc[$i]}= $info[$i]; + if ($column_name[$i] =~ /^(Create_|Update_|Check_)time$/) { + $Tables{$tablename}->{$column_name[$i]}= + &unsqltime($info[$i]); + } else { + $Tables{$tablename}->{$column_name[$i]}= $info[$i]; + } } # # Determine the column order @@ -535,12 +619,88 @@ sub update_table_info { =pod +=item &table_information() + +Inputs: table id + +Returns: hash with the table status + +=cut + +############################### +sub table_information { + my $table_id=shift; + if (&update_table_info($table_id)) { + return %{$Tables{$table_id}}; + } else { + return (); + } +} + +############################### + +=pod + +=item &col_order() + +Inputs: table id + +Returns: array with column order + +=cut + +############################### +sub col_order { + my $table_id=shift; + if (&update_table_info($table_id)) { + return @{$Tables{$table_id}->{'Col_order'}}; + } else { + return (); + } +} + +############################### + +=pod + =item &create_table() Inputs: - table description + table description, see &build_table_creation_request +Returns: + undef on error, table id on success. + +=cut + +############################### +sub create_table { + return undef if (!defined(&connect_to_db($dbh))); + my ($table_des)=@_; + my ($request,$table_id) = &build_table_creation_request($table_des); + # + # Execute the request to create the table + ############################################# + my $count = $dbh->do($request); + if (! defined($count)) { + $errorstring = "$dbh ATTEMPTED:\n".$request."\nRESULTING ERROR:\n". + $dbh->errstr(); + return undef; + } + my $tablename = &translate_id($table_id); + 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; +} -Input formats: +############################### + +=pod + +=item build_table_creation_request + +Input: table description table description = { permanent => 'yes' or 'no', @@ -572,14 +732,12 @@ Input formats: } -Returns: - undef on error, table id on success. +Returns: scalar string containing mysql commands to create the table =cut ############################### -sub create_table { - return undef if (!defined(&connect_to_db($dbh))); +sub build_table_creation_request { my ($table_des)=@_; # # Build request to create table @@ -659,23 +817,27 @@ sub create_table { 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; + $request .= "ENGINE=MYISAM"; + return $request,$table_id; +} + +############################### + +=pod + +=item &get_table_prefix() + +returns the cleaned version of user.name and user.domain for us in table names + +=cut + +############################### +sub get_table_prefix { + my $clean_name = $env{'user.name'}; + my $clean_domain = $env{'user.domain'}; + $clean_name =~ s/\W//g; + $clean_domain =~ s/\W//g; + return $clean_name.'_'.$clean_domain.'_'; } ############################### @@ -692,8 +854,9 @@ Used internally to prevent table name co sub get_new_table_id { my $newid = 0; my @tables = &tables_in_db(); + my $prefix = &get_table_prefix(); foreach (@tables) { - if (/^$ENV{'user.name'}_$ENV{'user.domain'}_(\d+)$/) { + if (/^\Q$prefix\E(\d+)$/) { $newid = $1 if ($1 > $newid); } } @@ -806,6 +969,59 @@ sub store_row { return 1; } + +############################### + +=pod + +=item &bulk_store_rows() + +Inputs: table id, [columns],[[row data1].[row data2],...] + +returns undef on error, 1 on success. + +=cut + +############################### +sub bulk_store_rows { + my ($table_id,$columns,$rows) = @_; + # + return undef if (! defined(&connect_to_db())); + my $dbh = &get_dbh(); + return undef if (! defined($dbh)); + 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 = 'INSERT IGNORE INTO '.$tablename.' '; + if (defined($columns) && ref($columns) eq 'ARRAY') { + $request .= join(',',@$columns).' '; + } + if (! defined($rows) || ref($rows) ne 'ARRAY') { + $errorstring = "no input rows given."; + return undef; + } + $request .= 'VALUES '; + foreach my $row (@$rows) { + # avoid doing row stuff here... + $request .= '('.join(',',@$row).'),'; + } + $request =~ s/,$//; + # $debugstring = "Executed ".$/.$request; # commented out - this is big + $dbh->do($request); + if ($dbh->err) { + $errorstring = 'Attempted '.$/.$request.$/.'Got error '.$dbh->errstr(); + return undef; + } + return 1; +} + + ############################### =pod @@ -879,22 +1095,37 @@ Returns undef on error. =cut ########################################### + +########## Show-Tables Cache +my $have_read_tables = 0; +my $dbh_sth; +########## + 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; + + ########## Show-Tables Cache + if(!$have_read_tables) { + $dbh_sth=$dbh->prepare('SHOW TABLES'); + $have_read_tables = 1; + } + $dbh_sth->execute(); + #$dbh_sth->execute(); # Removed strange execute - from release 119 + ########## + + my $aref = $dbh_sth->fetchall_arrayref; + if ($dbh_sth->err()) { + $errorstring = + "$dbh ATTEMPTED:\n".'fetchall_arrayref after SHOW TABLES'. + "\nRESULTING ERROR:\n".$dbh_sth->errstr; return undef; } - my $aref = $sth->fetchall_arrayref; - my @table_list=(); + my @table_list; foreach (@$aref) { - push @table_list,$_->[0]; + push(@table_list,$_->[0]); } - $debugstring = "Got list of tables in DB: @table_list"; - return @table_list; + $debugstring = "Got list of tables in DB: ".join(',',@table_list); + return(@table_list); } ########################################### @@ -917,7 +1148,7 @@ sub translate_id { # 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; + return &get_table_prefix().$id; } ########################################### @@ -973,11 +1204,10 @@ sub remove_from_table { return undef if (!defined(&connect_to_db())); # $table_id = &translate_id($table_id); - my $command = 'DELETE FROM '.$table_id.' WHERE '.$dbh->quote($column). + my $command = 'DELETE FROM '.$table_id.' WHERE '.$column. " LIKE BINARY ".$dbh->quote($value); my $sth = $dbh->prepare($command); - $sth->execute(); - if ($sth->err) { + unless ($sth->execute()) { $errorstring = "ERROR on execution of ".$command."\n".$sth->errstr; return undef; } @@ -986,6 +1216,82 @@ sub remove_from_table { return $rows; } +########################################### + +=pod + +=item drop_table($table_id) + +Issues a 'drop table if exists' command + +=cut + +########################################### + +sub drop_table { + my ($table_id) = @_; + return undef if (!defined(&connect_to_db())); + # + $table_id = &translate_id($table_id); + my $command = 'DROP TABLE IF EXISTS '.$table_id; + my $sth = $dbh->prepare($command); + $sth->execute(); + if ($sth->err) { + $errorstring = "ERROR on execution of ".$command."\n".$sth->errstr; + return undef; + } + $debugstring = $command; + delete($Tables{$table_id}); # remove any knowledge of the table + return 1; # if we got here there was no error, so return a 'true' value +} + +########################################## + +=pod + +=item fix_table_name + +Fixes a table name so that it will work with MySQL. + +=cut + +########################################## +sub fix_table_name { + my ($name) = @_; + $name =~ s/^(\d+[eE]\d+)/_$1/; + return $name; +} + + +# ---------------------------- convert 'time' format into a datetime sql format +sub sqltime { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = + localtime(&unsqltime($_[0])); + $mon++; $year+=1900; + return "$year-$mon-$mday $hour:$min:$sec"; +} + +sub maketime { + my %th=@_; + return POSIX::mktime(($th{'seconds'},$th{'minutes'},$th{'hours'}, + $th{'day'},$th{'month'}-1, + $th{'year'}-1900,0,0,$th{'dlsav'})); +} + + +######################################### +# +# Retro-fixing of un-backward-compatible time format + +sub unsqltime { + my $timestamp=shift; + if ($timestamp=~/^(\d+)\-(\d+)\-(\d+)\s+(\d+)\:(\d+)\:(\d+)$/) { + $timestamp=&maketime('year'=>$1,'month'=>$2,'day'=>$3, + 'hours'=>$4,'minutes'=>$5,'seconds'=>$6,'dlsav'=>-1); + } + return $timestamp; +} + 1;