747 lines
20 KiB
Perl
747 lines
20 KiB
Perl
|
#!/usr/bin/perl -w
|
||
|
#
|
||
|
# Generic database routines
|
||
|
#
|
||
|
|
||
|
|
||
|
package Golem;
|
||
|
use strict;
|
||
|
|
||
|
use Golem;
|
||
|
|
||
|
# courtesy of LiveJournal.org
|
||
|
sub disconnect_dbs {
|
||
|
foreach my $h (($Golem::DB, $Golem::PlanerDB, $Golem::CalendarDB, $Golem::OtrsDB, $Golem::SwerrsDB)) {
|
||
|
if ($h) {
|
||
|
$h->disconnect();
|
||
|
$h = undef;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
print STDERR localtime() . " [$$]: closed db connections\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
}
|
||
|
|
||
|
# build DSN connection string based on database info hashref,
|
||
|
# courtesy of livejournal.org
|
||
|
#
|
||
|
# $DBINFO = {
|
||
|
# 'master' => {
|
||
|
# 'dbname' => "golem_kohts",
|
||
|
# 'host' => "localhost",
|
||
|
# 'port' => 3306,
|
||
|
# 'user' => "root",
|
||
|
# 'pass' => "",
|
||
|
# 'sock' => "",
|
||
|
# 'encoding' => "utf8",
|
||
|
# },
|
||
|
# };
|
||
|
#
|
||
|
sub make_dbh_fdsn {
|
||
|
my ($db) = @_;
|
||
|
|
||
|
my $fdsn = "DBI:mysql";
|
||
|
$fdsn .= ":$db->{'dbname'}";
|
||
|
$fdsn .= ";host=$db->{'host'}" if $db->{'host'};
|
||
|
$fdsn .= ";port=$db->{'port'}" if $db->{'port'};
|
||
|
$fdsn .= ";mysql_socket=$db->{'sock'}" if $db->{'sock'};
|
||
|
$fdsn .= "|$db->{'user'}|$db->{'pass'}";
|
||
|
|
||
|
return $fdsn;
|
||
|
}
|
||
|
|
||
|
# test if connection is still available
|
||
|
# (should check for replication, etc. here)
|
||
|
#
|
||
|
sub connection_bad {
|
||
|
my ($dbh, $try) = @_;
|
||
|
|
||
|
return 1 unless $dbh;
|
||
|
|
||
|
my $ss = eval {
|
||
|
#
|
||
|
# $dbh->selectrow_hashref("SHOW SLAVE STATUS");
|
||
|
#
|
||
|
# on a real slave
|
||
|
#
|
||
|
# $ss = {
|
||
|
# 'Skip_counter' => '0',
|
||
|
# 'Master_Log_File' => 'ararita-bin.882',
|
||
|
# 'Connect_retry' => '60',
|
||
|
# 'Master_Host' => 'ararita.lenin.ru',
|
||
|
# 'Relay_Master_Log_File' => 'ararita-bin.882',
|
||
|
# 'Relay_Log_File' => 'laylah-relay-bin.323',
|
||
|
# 'Slave_IO_Running' => 'Yes',
|
||
|
# 'Slave_SQL_Running' => 'Yes',
|
||
|
# 'Master_Port' => '3306',
|
||
|
# 'Exec_master_log_pos' => '17720151',
|
||
|
# 'Relay_log_space' => '19098333',
|
||
|
# 'Relay_Log_Pos' => '19098333',
|
||
|
# 'Last_errno' => '0',
|
||
|
# 'Last_error' => '',
|
||
|
# 'Replicate_do_db' => 'prod_livejournal,prod_livejournal',
|
||
|
# 'Read_Master_Log_Pos' => '17720151',
|
||
|
# 'Master_User' => 'replication',
|
||
|
# 'Replicate_ignore_db' => ''
|
||
|
# };
|
||
|
|
||
|
$dbh->selectrow_hashref("select name from _dbi");
|
||
|
};
|
||
|
|
||
|
if ($dbh->err && $dbh->err != 1227) {
|
||
|
print STDERR localtime() . " [$$]: " . $dbh->errstr . "\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
if ($ss && $ss->{'name'} ne '??') {
|
||
|
return 0;
|
||
|
}
|
||
|
elsif ($ss && $ss->{'name'} eq '??') {
|
||
|
print STDERR localtime() . " [$$]: DBI returned garbage: $ss->{'name'}\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
return 1;
|
||
|
}
|
||
|
elsif (!$ss) {
|
||
|
print STDERR localtime() . " [$$]: DBI returned nothing\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# LJR modification; redefined in cgi-bin/Golem.pmGolem.pm
|
||
|
# so it works correctly with original LJ code
|
||
|
#
|
||
|
sub golem_get_db {
|
||
|
my ($params, $opts) = @_;
|
||
|
|
||
|
$opts = {} unless $opts;
|
||
|
$params = {} unless $params;
|
||
|
|
||
|
if ($Golem::DB) {
|
||
|
if (! connection_bad($Golem::DB)) {
|
||
|
return $Golem::DB;
|
||
|
}
|
||
|
else {
|
||
|
print STDERR localtime() . " [$$]: new connection: was bad\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
$Golem::DB->disconnect;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
print STDERR localtime() . " [$$]: new connection: had none\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
}
|
||
|
undef $Golem::DB;
|
||
|
|
||
|
# DB connection defaults (unless programmer specified them)
|
||
|
#
|
||
|
$params->{'RaiseError'} = 0 unless defined($params->{'RaiseError'});
|
||
|
$params->{'PrintError'} = 1 unless defined($params->{'PrintError'});
|
||
|
$params->{'AutoCommit'} = 1 unless defined($params->{'AutoCommit'});
|
||
|
|
||
|
Golem::die("No Golem::DBINFO master defined")
|
||
|
unless $Golem::DBINFO->{'master'};
|
||
|
|
||
|
my $dbinfo = $Golem::DBINFO->{'master'};
|
||
|
my $fdsn = make_dbh_fdsn($dbinfo);
|
||
|
|
||
|
$Golem::DB = DBI->connect($fdsn, $dbinfo->{'user'}, $dbinfo->{'pass'}, $params);
|
||
|
while (!$Golem::DB && $opts->{'retry_forever'}) {
|
||
|
Golem::do_log("database not available, retrying", {"stderr" => 1});
|
||
|
sleep 1;
|
||
|
$Golem::DB = DBI->connect($fdsn, $dbinfo->{'user'}, $dbinfo->{'pass'}, $params);
|
||
|
}
|
||
|
Golem::die("Unable to connect to database: " . DBI->errstr)
|
||
|
unless $Golem::DB;
|
||
|
|
||
|
$Golem::DB->do("SET NAMES " . $dbinfo->{'encoding'})
|
||
|
if $dbinfo->{'encoding'};
|
||
|
|
||
|
if (connection_bad($Golem::DB)) {
|
||
|
print STDERR "got fresh new bad handle, retrying\n" if $ENV{'GOLEM_DEBUG'};
|
||
|
$Golem::DB = undef;
|
||
|
$Golem::DB = Golem::get_db();
|
||
|
}
|
||
|
|
||
|
$Golem::default_dc_obj = Golem::get_dc($Golem::default_dc);
|
||
|
return $Golem::DB;
|
||
|
}
|
||
|
|
||
|
sub get_planer_db {
|
||
|
my ($params) = @_;
|
||
|
|
||
|
return $Golem::PlanerDB if $Golem::PlanerDB;
|
||
|
|
||
|
$params = {RaiseError => 0, PrintError => 1, AutoCommit => 1}
|
||
|
unless $params;
|
||
|
|
||
|
$Golem::PlanerDB = DBI->connect("DBI:Sybase:server=argo3.yandex.ru;database=planer;",
|
||
|
"helpdesk", "gkfyshfcnfvfys123", $params);
|
||
|
|
||
|
return $Golem::PlanerDB;
|
||
|
}
|
||
|
|
||
|
sub get_calendar_db {
|
||
|
my ($params) = @_;
|
||
|
|
||
|
return $Golem::CalendarDB if $Golem::CalendarDB;
|
||
|
|
||
|
$params = {RaiseError => 0, PrintError =>1, AutoCommit => 1}
|
||
|
unless $params;
|
||
|
|
||
|
$Golem::CalendarDB = DBI->connect("DBI:Sybase:server=argo3.yandex.ru;database=momdb;",
|
||
|
"staffreader", "cegthgfhjkm678", $params);
|
||
|
|
||
|
return $Golem::CalendarDB;
|
||
|
}
|
||
|
|
||
|
sub get_otrs_db {
|
||
|
my ($params) = @_;
|
||
|
|
||
|
return $Golem::OtrsDB if $Golem::OtrsDB;
|
||
|
|
||
|
$params = {RaiseError => 0, PrintError =>1, AutoCommit => 1}
|
||
|
unless $params;
|
||
|
|
||
|
$Golem::OtrsDB = DBI->connect("DBI:mysql:database=otrs_utf8:host=casa.yandex.ru:port=3306",
|
||
|
"userorder", "xuo9Bahf", $params);
|
||
|
|
||
|
return $Golem::OtrsDB;
|
||
|
}
|
||
|
|
||
|
sub get_swerrs_db {
|
||
|
my ($params) = @_;
|
||
|
|
||
|
return $Golem::SwerrsDB if $Golem::SwerrsDB;
|
||
|
|
||
|
$params = { RaiseError => 0, PrintError => 1, AutoCommit => 1 }
|
||
|
unless $params;
|
||
|
|
||
|
$Golem::SwerrsDB = DBI->connect("DBI:mysql:racktables:localhost:3306", "swerrs", "V7Hl}O]Usr", $params);
|
||
|
|
||
|
return $Golem::SwerrsDB;
|
||
|
}
|
||
|
|
||
|
|
||
|
sub sth_bind_array {
|
||
|
my ($sth, $bound_values) = @_;
|
||
|
|
||
|
my $i = 0;
|
||
|
foreach my $b (@{$bound_values}) {
|
||
|
$i++;
|
||
|
|
||
|
Golem::die("error binding params")
|
||
|
unless $sth->bind_param($i, $b) ;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# courtesy of LiveJournal.org
|
||
|
# see also: http://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_last-insert-id
|
||
|
#
|
||
|
sub alloc_global_counter {
|
||
|
my ($tag, $recurse) = @_;
|
||
|
|
||
|
my $dbh = Golem::get_db();
|
||
|
my $newmax;
|
||
|
|
||
|
# in case name `counter` is already occupied
|
||
|
# by some user table
|
||
|
my $counter_prefix = "";
|
||
|
$counter_prefix = $Golem::counter_prefix
|
||
|
if defined($Golem::counter_prefix);
|
||
|
|
||
|
my $rs = $dbh->do("UPDATE ${counter_prefix}counter SET max=LAST_INSERT_ID(max+1) WHERE tag=?", undef, $tag);
|
||
|
if ($rs > 0) {
|
||
|
$newmax = $dbh->selectrow_array("SELECT LAST_INSERT_ID()");
|
||
|
return $newmax;
|
||
|
}
|
||
|
|
||
|
return undef if $recurse;
|
||
|
|
||
|
# no prior counter rows - initialize one.
|
||
|
|
||
|
# if this is a table then trying default id column
|
||
|
if ($Golem::SCHEMA_CACHE->{'tables'}->{$tag}) {
|
||
|
$newmax = $dbh->selectrow_array("SELECT MAX(id) FROM `$tag`");
|
||
|
}
|
||
|
else {
|
||
|
Golem::die("alloc_global_counter: unknown tag [$tag], unable to get max value.");
|
||
|
}
|
||
|
|
||
|
$newmax += 0;
|
||
|
|
||
|
$dbh->do("INSERT IGNORE INTO ${counter_prefix}counter (tag, max) VALUES (?,?)",
|
||
|
undef, $tag, $newmax) || return undef;
|
||
|
|
||
|
return Golem::alloc_global_counter($tag, 1);
|
||
|
}
|
||
|
|
||
|
# get schema table definition,
|
||
|
# prepare in-memory table structure
|
||
|
#
|
||
|
sub get_schema_table {
|
||
|
my ($table_name, $opts) = @_;
|
||
|
|
||
|
return $Golem::SCHEMA_CACHE->{'tables'}->{$table_name}
|
||
|
if $Golem::SCHEMA_CACHE->{'tables'}->{$table_name} &&
|
||
|
!$opts->{'force'};
|
||
|
|
||
|
delete($Golem::SCHEMA_CACHE->{'tables'}->{$table_name})
|
||
|
if $Golem::SCHEMA_CACHE->{'tables'}->{$table_name};
|
||
|
|
||
|
$Golem::SCHEMA_CACHE->{'tables'}->{$table_name}->{'fields'} = {};
|
||
|
my $t = $Golem::SCHEMA_CACHE->{'tables'}->{$table_name};
|
||
|
|
||
|
my $dbh = Golem::get_db();
|
||
|
Golem::debug_sql("describe `$table_name`");
|
||
|
my $sth = $dbh->prepare("describe `$table_name`");
|
||
|
$sth->execute();
|
||
|
Golem::die("Error describing table [$table_name]: " . $dbh->errstr)
|
||
|
if $dbh->err;
|
||
|
|
||
|
my $select_all_sql = "";
|
||
|
while (my $r = $sth->fetchrow_hashref) {
|
||
|
my $field_name = $r->{'Field'};
|
||
|
|
||
|
$t->{'fields'}->{$field_name} = $r;
|
||
|
|
||
|
if ($r->{'Type'} =~ /^enum\((.+)\)/o) {
|
||
|
my $enums = $1;
|
||
|
foreach my $etype (split(/,/o, $enums)) {
|
||
|
$etype =~ s/'//go;
|
||
|
$t->{'fields'}->{$field_name}->{'enum'}->{$etype} = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($r->{'Type'} eq 'timestamp') {
|
||
|
$select_all_sql .= "UNIX_TIMESTAMP(`$field_name`) as `$field_name`, ";
|
||
|
}
|
||
|
else {
|
||
|
$select_all_sql .= "`$field_name`, ";
|
||
|
}
|
||
|
|
||
|
if ($r->{'Key'} eq 'PRI') {
|
||
|
$t->{'primary_key'}->{$field_name} = 1;
|
||
|
}
|
||
|
}
|
||
|
chop($select_all_sql);
|
||
|
chop($select_all_sql);
|
||
|
|
||
|
$Golem::SCHEMA_CACHE->{'tables'}->{$table_name}->{'select_all_sql'} = $select_all_sql;
|
||
|
|
||
|
return $Golem::SCHEMA_CACHE->{'tables'}->{$table_name};
|
||
|
}
|
||
|
|
||
|
# function tells whether field is data field or some special field
|
||
|
# like host.id (incremented with alloc_global_counter) or
|
||
|
# like host.last_updated (automatically updated when record is updated)
|
||
|
# maybe we should filter them by name instead of using db structure hints?
|
||
|
sub is_data_field {
|
||
|
my ($table_name, $field_name, $opts) = @_;
|
||
|
|
||
|
$opts = {} unless $opts;
|
||
|
|
||
|
my $table = Golem::get_schema_table($table_name);
|
||
|
my $table_fields = $table->{'fields'};
|
||
|
|
||
|
if ($table_fields->{$field_name}) {
|
||
|
if ($table_fields->{$field_name}->{'Default'} &&
|
||
|
$table_fields->{$field_name}->{'Default'} eq 'CURRENT_TIMESTAMP' &&
|
||
|
!$opts->{'ignore_default_current_timestamp'} ) {
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
# if ($table_fields->{$field_name}->{'Key'} &&
|
||
|
# $table_fields->{$field_name}->{'Key'} eq 'PRI') {
|
||
|
#
|
||
|
# return 0;
|
||
|
# }
|
||
|
|
||
|
# we have to distinguish between host.id and host_rackmap.host;
|
||
|
# both are PRIMARY keys, but
|
||
|
# 1) we shouldn't ever update host.id
|
||
|
# 2) we have to update host_rackmap.host with host.id
|
||
|
# when creating record corresponding to host record
|
||
|
if ($field_name eq "id" && !$opts->{'manual_id_management'}) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub __insert {
|
||
|
my ($table_name, $record_hashref, $opts) = @_;
|
||
|
|
||
|
Golem::die("Severe programmer error: __insert expects table name as first parameter!")
|
||
|
unless $table_name;
|
||
|
Golem::die("Severe programmer error: __insert expects record hashref as second parameter!")
|
||
|
unless ref($record_hashref) eq 'HASH';
|
||
|
|
||
|
$opts = {} unless ref($opts) eq 'HASH';
|
||
|
|
||
|
my $dbh;
|
||
|
if ($opts->{'dbh'}) {
|
||
|
$dbh = $opts->{'dbh'};
|
||
|
}
|
||
|
else {
|
||
|
$dbh = Golem::get_db();
|
||
|
}
|
||
|
|
||
|
$dbh->{'PrintError'} = $opts->{'PrintError'}
|
||
|
if defined($opts->{'PrintError'});
|
||
|
|
||
|
my $sth;
|
||
|
|
||
|
my $table = Golem::get_schema_table($table_name);
|
||
|
my $table_fields = $table->{'fields'};
|
||
|
|
||
|
# continue only if there's data for the table
|
||
|
# or if there's a flag saying we should create
|
||
|
# empty record with defaults
|
||
|
my $have_data_for_the_table = 0;
|
||
|
while (my ($o, $v) = each(%{$record_hashref})) {
|
||
|
if (Golem::is_data_field($table_name, $o, $opts)) {
|
||
|
$have_data_for_the_table = 1;
|
||
|
}
|
||
|
}
|
||
|
unless ($have_data_for_the_table || $opts->{'create_empty_record'}) {
|
||
|
return $dbh;
|
||
|
}
|
||
|
|
||
|
my @record_fields;
|
||
|
my @record_values;
|
||
|
|
||
|
foreach my $o (keys %{$record_hashref}) {
|
||
|
# $record_hashref might contain more fields than present in database.
|
||
|
# we only choose those which are in db
|
||
|
|
||
|
if ($table_fields->{$o} && Golem::is_data_field($table_name, $o, $opts)) {
|
||
|
|
||
|
# enum validation
|
||
|
if ($table_fields->{$o}->{'enum'}) {
|
||
|
Golem::die("Enum [$table_name.$o] value is not specified and doesn't have default value")
|
||
|
if !defined($record_hashref->{$o}) && $table_fields->{$o}->{'Default'} eq '';
|
||
|
|
||
|
Golem::die("Enum [$table_name.$o] can't be [$record_hashref->{$o}]")
|
||
|
if $record_hashref->{$o} && !$table_fields->{$o}->{'enum'}->{$record_hashref->{$o}};
|
||
|
|
||
|
# if they passed empty value for enum
|
||
|
# and there's some default -- silently
|
||
|
# decide to use it
|
||
|
unless ($record_hashref->{$o}) {
|
||
|
delete($record_hashref->{$o});
|
||
|
next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
push @record_fields, $o;
|
||
|
push @record_values, $record_hashref->{$o};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($table_fields->{"id"} && !$opts->{'manual_id_management'}) {
|
||
|
if ($record_hashref->{"id"}) {
|
||
|
Golem::die("Severe database structure or programmer error: __insert got id [$record_hashref->{'id'}]
|
||
|
when creating record for table [$table_name]; won't overwrite.\n");
|
||
|
}
|
||
|
|
||
|
$record_hashref->{"id"} = Golem::alloc_global_counter($table_name);
|
||
|
|
||
|
# check that id is not taken and
|
||
|
# die with severe error otherwise
|
||
|
#
|
||
|
|
||
|
my $t_id = $dbh->selectrow_array("select id from `$table_name` where id = ?",
|
||
|
undef, $record_hashref->{"id"});
|
||
|
if ($t_id && $t_id eq $record_hashref->{"id"}) {
|
||
|
Golem::die("Severe database error: __insert got [$t_id] for table [$table_name] " .
|
||
|
"from alloc_global_counter which already exists!\n" .
|
||
|
"Probable somebody is populating [$table_name] without Golem::__insert()\n");
|
||
|
}
|
||
|
|
||
|
push @record_fields, "id";
|
||
|
push @record_values, $record_hashref->{"id"};
|
||
|
}
|
||
|
|
||
|
my $sql;
|
||
|
my @bound_values;
|
||
|
|
||
|
$sql = "INSERT INTO `$table_name` ( ";
|
||
|
foreach my $o (@record_fields) {
|
||
|
$sql = $sql . " `$o`,";
|
||
|
}
|
||
|
chop($sql);
|
||
|
|
||
|
$sql .= " ) VALUES ( ";
|
||
|
|
||
|
my $i = 0;
|
||
|
foreach my $o (@record_values) {
|
||
|
|
||
|
# we represent timestamp datatype as unixtime (http://en.wikipedia.org/wiki/Unix_time)
|
||
|
# doing all the conversions almost invisible to the end user
|
||
|
#
|
||
|
# if the value being written is 0 then we're not using FROM_UNIXTIME(value)
|
||
|
# (which generates warnings) just value
|
||
|
#
|
||
|
if ($table_fields->{$record_fields[$i]}->{'Type'} eq 'timestamp' && $o && $o != 0) {
|
||
|
Golem::die("Programmer error: __insert got hashref with invalid data for $table_name.$record_fields[$i] (should be unixtime)")
|
||
|
unless $o =~ /^[0-9]+$/o;
|
||
|
|
||
|
$sql = $sql . "FROM_UNIXTIME(?),";
|
||
|
}
|
||
|
else {
|
||
|
$sql = $sql . "?,";
|
||
|
}
|
||
|
|
||
|
push @bound_values, $o;
|
||
|
$i++;
|
||
|
}
|
||
|
chop($sql);
|
||
|
$sql .= " )";
|
||
|
|
||
|
Golem::debug_sql($sql, \@bound_values);
|
||
|
|
||
|
$sth = $dbh->prepare($sql);
|
||
|
Golem::sth_bind_array($sth, \@bound_values);
|
||
|
$sth->execute();
|
||
|
|
||
|
if ($dbh->err && $dbh->{'PrintError'}) {
|
||
|
Golem::do_log("got error [" . $dbh->err . "] [" . $dbh->errstr . "]" .
|
||
|
" while executing [$sql] with values (" . join(",", @bound_values) . ")",
|
||
|
{'stderr' => 1});
|
||
|
}
|
||
|
|
||
|
return $dbh;
|
||
|
}
|
||
|
|
||
|
sub __update {
|
||
|
my ($table_name, $record_hashref, $opts) = @_;
|
||
|
|
||
|
Golem::die("Severe programmer error: __update expects table name as first parameter!")
|
||
|
unless $table_name;
|
||
|
Golem::die("Severe programmer error: __update expects record hashref as second parameter!")
|
||
|
unless ref($record_hashref) eq 'HASH';
|
||
|
|
||
|
$opts = {} unless ref($opts) eq 'HASH';
|
||
|
|
||
|
my $dbh;
|
||
|
if ($opts->{'dbh'}) {
|
||
|
$dbh = $opts->{'dbh'};
|
||
|
}
|
||
|
else {
|
||
|
$dbh = Golem::get_db();
|
||
|
}
|
||
|
|
||
|
my $sth;
|
||
|
|
||
|
my $table = Golem::get_schema_table($table_name);
|
||
|
my $table_fields = $table->{'fields'};
|
||
|
my $unique_fields_arrayref = [keys %{$table->{'primary_key'}}];
|
||
|
|
||
|
if ($opts->{'unique_fields'}) {
|
||
|
$unique_fields_arrayref = $opts->{'unique_fields'};
|
||
|
}
|
||
|
|
||
|
# continue only if there's data for the table
|
||
|
# in the in-memory hash or if there's a flag
|
||
|
# saying we should create empty record with defaults
|
||
|
#
|
||
|
my $have_data_for_the_table = 0;
|
||
|
while (my ($o, $v) = each(%{$record_hashref})) {
|
||
|
if (Golem::is_data_field($table_name, $o)) {
|
||
|
my $is_unique = 0;
|
||
|
|
||
|
foreach my $u (@{$unique_fields_arrayref}) {
|
||
|
if ($u eq $o) {
|
||
|
$is_unique = 1;
|
||
|
}
|
||
|
}
|
||
|
next if $is_unique;
|
||
|
|
||
|
$have_data_for_the_table = 1;
|
||
|
}
|
||
|
}
|
||
|
unless ($have_data_for_the_table || $opts->{'create_empty_record'}) {
|
||
|
return $dbh;
|
||
|
}
|
||
|
|
||
|
my $sql;
|
||
|
my @bound_values;
|
||
|
|
||
|
$sql = "SELECT " . $table->{'select_all_sql'} . " from `$table_name` WHERE ";
|
||
|
foreach my $f (@{$unique_fields_arrayref}) {
|
||
|
if ($table_fields->{$f}->{'Type'} eq 'timestamp' && $record_hashref->{$f} != 0) {
|
||
|
Golem::die("Programmer error: __update got hashref with invalid data for $table_name.$f (should be unixtime)")
|
||
|
unless $record_hashref->{$f} =~ /^[0-9]+$/o;
|
||
|
|
||
|
$sql .= " `$f` = FROM_UNIXTIME(?) and ";
|
||
|
}
|
||
|
else {
|
||
|
$sql .= " `$f` = ? and ";
|
||
|
}
|
||
|
push @bound_values, $record_hashref->{$f};
|
||
|
}
|
||
|
# remove last "and "
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
|
||
|
$sth = $dbh->prepare($sql);
|
||
|
Golem::sth_bind_array($sth, \@bound_values);
|
||
|
$sth->execute();
|
||
|
|
||
|
# create record if it doesn't exist: useful when updating
|
||
|
# records in dependent tables (hosts_resps, hosts_netmap, host_rackmap)
|
||
|
# when master table exists.
|
||
|
unless ($sth->rows) {
|
||
|
if ($opts->{"create_nonexistent"}) {
|
||
|
$dbh = Golem::__insert($table_name, $record_hashref, $opts);
|
||
|
return $dbh;
|
||
|
}
|
||
|
else {
|
||
|
Golem::die("Programmer error: requested to update non-existent record with no create_nonexistent option");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
my $existing_row;
|
||
|
while(my $r = $sth->fetchrow_hashref()) {
|
||
|
Golem::debug_sql($sql, \@bound_values);
|
||
|
Golem::die("more than 1 record fetched with should-be-unique lookup")
|
||
|
if $existing_row;
|
||
|
|
||
|
$existing_row = $r;
|
||
|
}
|
||
|
|
||
|
# check that existing record differs somehow from record to be written
|
||
|
my $records_differ = 0;
|
||
|
while (my ($k, $v) = each %{$existing_row}) {
|
||
|
if (Golem::is_data_field($table_name, $k)) {
|
||
|
|
||
|
# what a mess!
|
||
|
utf8::decode($record_hashref->{$k});
|
||
|
utf8::decode($v);
|
||
|
|
||
|
if (
|
||
|
($record_hashref->{$k} && $v && $v ne $record_hashref->{$k}) ||
|
||
|
(! $record_hashref->{$k} && $v) ||
|
||
|
($record_hashref->{$k} && ! $v)
|
||
|
) {
|
||
|
|
||
|
Golem::debug_sql("in-memory [$table_name] object field [$k] differs: [" .
|
||
|
($record_hashref->{$k} ? $record_hashref->{$k} : "") . "
|
||
|
] -- [" .
|
||
|
($v ? $v : "") .
|
||
|
"]");
|
||
|
|
||
|
$records_differ = 1;
|
||
|
last;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# don't update database if that wouldn't actually
|
||
|
# change any data; we should save A LOT of time here
|
||
|
#
|
||
|
return $dbh unless $records_differ;
|
||
|
|
||
|
@bound_values = ();
|
||
|
$sql = "";
|
||
|
while (my ($o, $v) = each(%{$record_hashref})) {
|
||
|
# $record_hashref might contain more fields than present in database.
|
||
|
# we only choose those which are in db
|
||
|
if ($table_fields->{$o} && Golem::is_data_field($table_name, $o)) {
|
||
|
if ($table_fields->{$o}->{'Type'} eq 'timestamp' && $record_hashref->{$o} && $record_hashref->{$o} != 0) {
|
||
|
Golem::die("Programmer error: __update got hashref with invalid data for $table_name.$o (should be unixtime)")
|
||
|
unless $record_hashref->{$o} =~ /^[0-9]+$/o;
|
||
|
|
||
|
$sql = $sql . " `$o` = FROM_UNIXTIME(?),";
|
||
|
}
|
||
|
else {
|
||
|
$sql = $sql . " `$o` = ?,";
|
||
|
}
|
||
|
push @bound_values, $record_hashref->{$o};
|
||
|
}
|
||
|
}
|
||
|
chop($sql);
|
||
|
|
||
|
$sql = "UPDATE `$table_name` SET " . $sql . " WHERE ";
|
||
|
foreach my $f (@{$unique_fields_arrayref}) {
|
||
|
$sql .= " `$f` = ? and ";
|
||
|
push @bound_values, $record_hashref->{$f};
|
||
|
}
|
||
|
# remove last "and "
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
|
||
|
Golem::debug_sql($sql, \@bound_values);
|
||
|
|
||
|
$sth = $dbh->prepare($sql);
|
||
|
Golem::sth_bind_array($sth, \@bound_values);
|
||
|
$sth->execute();
|
||
|
|
||
|
if ($dbh->err) {
|
||
|
Golem::do_log("error executing: $sql; bound values: " . join(",", @bound_values), {"stderr" => 1});
|
||
|
}
|
||
|
|
||
|
return $dbh;
|
||
|
}
|
||
|
|
||
|
|
||
|
sub __delete {
|
||
|
my ($table_name, $record_hashref, $opts) = @_;
|
||
|
|
||
|
Golem::die("Severe programmer error: __delete expects table name as first parameter!")
|
||
|
unless $table_name;
|
||
|
Golem::die("Severe programmer error: __delete expects record hashref as second parameter!")
|
||
|
unless ref($record_hashref) eq 'HASH';
|
||
|
|
||
|
$opts = {} unless ref($opts) eq 'HASH';
|
||
|
|
||
|
my $dbh;
|
||
|
if ($opts->{'dbh'}) {
|
||
|
$dbh = $opts->{'dbh'};
|
||
|
}
|
||
|
else {
|
||
|
$dbh = Golem::get_db();
|
||
|
}
|
||
|
|
||
|
my $sth;
|
||
|
|
||
|
my $table = Golem::get_schema_table($table_name);
|
||
|
my $table_fields = $table->{'fields'};
|
||
|
my $unique_fields_arrayref = [keys %{$table->{'primary_key'}}];
|
||
|
|
||
|
if ($opts->{'unique_fields'}) {
|
||
|
$unique_fields_arrayref = $opts->{'unique_fields'};
|
||
|
}
|
||
|
|
||
|
my @bound_values = ();
|
||
|
my $sql = "DELETE FROM `$table_name` WHERE ";
|
||
|
|
||
|
foreach my $f (@{$unique_fields_arrayref}) {
|
||
|
$sql .= " `$f` = ? and ";
|
||
|
push @bound_values, $record_hashref->{$f};
|
||
|
}
|
||
|
# remove last "and "
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
chop($sql);
|
||
|
|
||
|
$sth = $dbh->prepare($sql);
|
||
|
Golem::sth_bind_array($sth, \@bound_values);
|
||
|
$sth->execute();
|
||
|
|
||
|
if ($dbh->err) {
|
||
|
Golem::do_log("error executing: $sql; bound values: " . join(",", @bound_values), {"stderr" => 1});
|
||
|
}
|
||
|
|
||
|
return $dbh;
|
||
|
}
|
||
|
|
||
|
|
||
|
1;
|