Commits

Anonymous committed 2787c97

[svn r74] New branch for working on post-release improvements.

Comments (0)

Files changed (8)

branches/daybobase-0.2.0/daybobase-test.ini

+;
+; This is a sample configuration
+; for testing the DayboBase component
+;
+
+[database]
+host=localhost
+hostro=localhost
+db=ddrp_daybo
+user=root
+pass=
+type=mysql

branches/daybobase-0.2.0/daybobase-test.pl

+#!/usr/bin/perl -w
+#
+# This is a testbed for the DayboBase package.  It tests that we can
+# connect to the Daybo Logic database with some sample credentials
+# which just allow us to do a very simple area-code lookup.
+#
+#----------------------------------------------------------------------------
+use strict;
+use warnings;
+use Data::Dumper;
+use Test::More tests => 16;
+
+BEGIN { use_ok( 'DayboBase' ); }
+
+use constant EXIT_FAILURE => (1);
+use constant EXIT_SUCCESS => (0);
+use constant DEBUG => (0);
+
+use constant FAKE_TEST_UUID => '77475a3a-1529-11e0-bfde-0022150093ff';
+
+# Sample queries for various tests
+use constant QUERY_ALL_UNIX_USERS => 'SELECT * FROM unix_users ORDER BY uid';
+use constant QUERY_ALL_COUNTRY_CODES => 'SELECT * FROM country_codes';
+use constant QUERY_BOUND_UID => 'SELECT username FROM unix_users WHERE uid = ?';
+use constant QUERY_BOUND_UID_GID => 'SELECT username FROM unix_users WHERE uid = ? AND gid = ?';
+#----------------------------------------------------------------------------
+sub NewMain();
+sub Main();
+sub TestPrepare($);
+sub test_Handle();
+sub test_Query();
+sub test_Prepare();
+sub test_Unprepare();
+sub test_Execute();
+#----------------------------------------------------------------------------
+sub NewMain()
+{
+  my %tests = (
+    'Handle' => { 'C' => \&test_Handle },
+    'Query' => { 'C' => \&test_Query },
+    'Prepare' => { 'C' => \&test_Prepare },
+    'Unprepare' => { 'C' => \&test_Unprepare },
+    'UnprepareAll' => { 'C' => \&test_UnprepareAll },
+    'Execute' => { 'C' => \&test_Execute },
+    'GetLastErrorMsg' => { 'C' => \&test_GetLastErrorMsg },
+    'LastQuerySpeed' => { 'C' => \&test_LastQuerySpeed },
+    'FinishQuery' => { 'C' => \&test_FinishQuery },
+    'Exceptions' => { 'C' => \&test_Exceptions },
+    'Ping' => { 'C' => \&test_Ping },
+    'GetCredentials' => { 'C' => \&test_GetCredentials },
+    'Rows' => { 'C' => \&test_Rows },
+    'GetNextRow' => { 'C' => \&test_GetNextRow },
+    'ValidateStatementHandle' => { 'C' => \&test_ValidateStatementHandle }
+  );
+  foreach my $k ( keys(%tests) ) {
+    $tests{$k}->{'R'} = 1;
+    is($tests{$k}->{'C'}->(), $tests{$k}->{'R'}, $k);
+  }
+}
+#----------------------------------------------------------------------------
+sub test_Handle()
+{
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  return 1 if ( $dbh );
+  return 0;
+}
+#----------------------------------------------------------------------------
+sub test_Query()
+{
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    return 1 if ( $dbh->Query(QUERY_BOUND_UID(), 1101) );
+  }
+  return 0;
+}
+#----------------------------------------------------------------------------
+sub test_Prepare()
+{
+  my %queries = (
+    1 => { 'Q' => QUERY_BOUND_UID() },
+    2 => { 'Q' => QUERY_BOUND_UID_GID() },
+    3 => { 'Q' => QUERY_ALL_UNIX_USERS() }
+  );
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    # Prepare the queries
+    foreach my $k ( keys(%queries) ) {
+      my $h = $dbh->Prepare($queries{$k}->{'Q'});
+      $queries{$k}->{'H'} = $h;
+    }
+    # Call the validator on the handles
+    foreach my $k ( keys(%queries) ) {
+      my $h = $queries{$k}->{'H'};
+      $queries{$k}->{'V'} = $dbh->ValidateStatementHandle($h);
+    }
+  }
+
+  # Check that all handles were valid
+  foreach my $k ( keys(%queries) ) {
+    return 0 unless ( $queries{$k}->{'V'} );
+  }
+  return 1;
+}
+#----------------------------------------------------------------------------
+sub test_Unprepare()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    my $sth = $dbh->Prepare(QUERY_ALL_UNIX_USERS());
+    if ( $dbh->ValidateStatementHandle($sth) ) {
+      $ret = 1 if ( $dbh->Unprepare($sth) );
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_UnprepareAll()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    my $expect = $dbh->Prepare(QUERY_ALL_UNIX_USERS());
+    my $actual = $dbh->UnprepareAll();
+    $ret = 1
+      if ( ($expect && $actual == 1) || (!$expect && $actual == 0) );
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_Execute()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    my $sth = $dbh->Prepare(QUERY_ALL_UNIX_USERS());
+    $ret = 1 if ( $dbh->Execute() );
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_GetLastErrorMsg()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    my $recvMsg;
+    my $testMsg = FAKE_TEST_UUID();
+    $dbh->_SetLastErrorMsg($testMsg);
+    $recvMsg = $dbh->GetLastErrorMsg();
+    $ret = 1 if ( $recvMsg && $recvMsg eq $testMsg );
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_LastQuerySpeed()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    my @tests = ( 0, 0.01, 0.001, 23.34, -0.1 );
+    foreach my $t ( @tests ) {
+      my $x;
+      $dbh->LastQuerySpeed($t); # Set
+      $x = $dbh->LastQuerySpeed();
+      if ( defined($x) && $x =~ m/^\d+(\.\d+)?$/ ) {
+        if ( $t >= 0 ) {
+          $ret++ if ( $t == $x );
+        } else {
+          $ret++ if ( $x == 0 );
+        }
+      }
+    }
+    if ( $ret == scalar(@tests) ) {
+      $ret = 1;
+    } else {
+      $ret = 0;
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_FinishQuery()
+{
+  # First let's do a query we know will retrieve more than one row.
+  my $ret = 0;
+  my $dbh = DayboBase::Handle({'Debug' => DEBUG()});
+  if ( $dbh ) {
+    my $sth = $dbh->Query(QUERY_ALL_UNIX_USERS());
+    if ( $sth ) {
+      $ret = 1 if ( $dbh->GetNextRow() ); # Got one row?
+      $dbh->FinishQuery(); # OK, there should be no rows now.
+      # Try to fetch them regardless
+      $dbh->Exceptions(0); # Switch off exceptions.
+      while ( my $ref = $dbh->GetNextRow() ) {
+        $ret = 0;
+      }
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_Exceptions()
+{
+  # Tricky function this, we have to capture exceptions if raised.
+  my $ret = 0;
+  my $dbh = DayboBase::Handle(); # Get database handle
+  if ( $dbh ) {
+    # Exceptions are on by default... verify!
+    return $ret unless ( $dbh->Exceptions() );
+    # Now let's capture an exception
+    eval {
+      $dbh->GetNextRow();
+    };
+    unless ( $@ ) { # No exception
+      return $ret; # Fail!
+    }
+    # Now let's turn off exception handling.
+    return $ret if ( $dbh->Exceptions(0) );
+    $dbh->GetNextRow(); # Illegal, but will not throw an exception now.
+    $ret = 1;
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_Ping()
+{
+  my $dbh = DayboBase::Handle();
+  if ( $dbh ) {
+    return $dbh->Ping();
+  }
+  return 0;
+}
+#----------------------------------------------------------------------------
+sub test_GetCredentials()
+{
+  my $ret = 0;
+  my %creds = ( );
+  my $dbh = DayboBase::Handle();
+  if ( $dbh ) {
+    if ( $dbh->GetCredentials(\%creds) ) {
+      my @req = ( 'pass', 'db', 'user', 'host', 'hostro', 'type' );
+      foreach my $r ( @req ) {
+        return $ret unless ( defined($creds{$r}) );
+      }
+      $ret = 1;
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_Rows()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle();
+  if ( $dbh ) {
+    my $sth = $dbh->Query(QUERY_ALL_UNIX_USERS());
+    if ( $sth ) {
+      my $n = $dbh->Rows();
+      $ret = 1 if ( $n == 2 );
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_GetNextRow()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle();
+  if ( $dbh ) {
+    my $maxkey;
+    my $sth;
+    my %real = ( ); # Real data from database.
+    my %expect = (
+      1 => { 'uid' => 1101, 'username' => 'overlord' },
+      2 => { 'uid' => 5598, 'username' => 'palmer' }
+    );
+    # Calculate the maximum key above.
+    $maxkey = 0;
+    foreach my $k ( keys(%expect) ) {
+      $maxkey = $k if ( $k > $maxkey );
+    }
+
+    # Run the query
+    $sth = $dbh->Query(QUERY_ALL_UNIX_USERS());
+
+    # Read all the rows back into an easily parsable format.
+    if ( $sth ) {
+      my $n = 1;
+      while ( my $ref = $dbh->GetNextRow() ) {
+        foreach my $k ( keys(%$ref) ) {
+          $real{$n}->{$k} = $ref->{$k};
+        }
+        $n++;
+      }
+    }
+
+    # Check for incorrect row count
+    return $ret if ( $maxkey != scalar(keys(%real)) );
+
+    # Verify the rows with hard-coded information we expect.
+    for ( my $ri = 1; $ri <= $maxkey; $ri++ ) {
+      $ret = 1;
+      foreach my $k ( keys( %{ $expect{$maxkey} } ) ) {
+        if ( $real{$ri}->{$k} ne $expect{$ri}->{$k} ) {
+          printf("RI: %u, K: %s\n", $ri, $k);
+          print Dumper \%real;
+          $ret = 0;
+          return $ret;
+        }
+      }
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub test_ValidateStatementHandle()
+{
+  my $ret = 0;
+  my $dbh = DayboBase::Handle();
+  if ( $dbh ) {
+    my $fakeHandle = FAKE_TEST_UUID();
+    $ret = 1 unless ( $dbh->ValidateStatementHandle($fakeHandle) );
+
+    if ( $ret ) {
+      my @sths = ( undef, undef );
+      $sths[0] = $dbh->Prepare(QUERY_ALL_COUNTRY_CODES());
+      $sths[1] = $dbh->Prepare(QUERY_ALL_UNIX_USERS());
+      foreach my $s ( @sths ) {
+        $ret = 0 unless ( $dbh->ValidateStatementHandle($s) );
+      }
+    }
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub Main()
+{
+  my $ret = EXIT_FAILURE();
+  my $query = 'SELECT cn,cc FROM country_codes WHERE enabled = 1';
+  my $dbh = DayboBase::Handle({'Debug' => 1});
+  printf("Running query: %s\n", $query);
+  if ( $dbh->Query($query) ) {
+    my $rows = $dbh->Rows();
+    printf(
+      "Query successful; %u rows returned in %.2f secs.\n",
+      $rows, $dbh->LastQuerySpeed()
+    );
+    if ( $rows > 0 ) {
+      my $ireland = '';
+      while ( my $ref = $dbh->GetNextRow(undef) ) {
+        next if ( $ref->{'cc'} ne 'IE' );
+        $ireland = $ref->{'cn'};
+        # Normally we'd say 'last' for efficiency, but we want to make sure GetNextRow()
+        # returns undef, so there there is no infinite loop condition in the library.
+      }
+      printf("Got IE: %s\n", $ireland);
+      $ret = EXIT_SUCCESS() if ( $ireland eq 'Ireland' );
+    }
+    $dbh->FinishQuery();
+  } else {
+    printf("Query failed: %s.\n", $dbh->GetLastErrorMsg());
+  }
+
+  TestPrepare($dbh);
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub TestPrepare($)
+{
+  my $Dbh = $_[0];
+  my @queries = (
+    'SELECT * FROM unix_users',
+    'SELECT * FROM country_codes',
+    'SELECT username FROM unix_users WHERE uid = ?',
+    'SELECT username FROM unix_users WHERE uid = ? AND gid = ?'
+  );
+
+  foreach my $q ( @queries ) {
+    $Dbh->Unprepare($Dbh->Prepare($q));
+  }
+}
+#----------------------------------------------------------------------------
+exit(NewMain());
+#----------------------------------------------------------------------------

branches/daybobase-0.2.0/debian/changelog

+libdaybobase-perl (0.1.0) stable; urgency=low
+
+  * Initial version (not well tested in the field, but passing internal tests)
+
+ -- David Palmer <palmer@overchat.org>  Sat, 01 Jan 2011 17:52:21 +0000

branches/daybobase-0.2.0/debian/compat

+7

branches/daybobase-0.2.0/debian/control

+Source: libdaybobase-perl
+Section: perl
+Priority: extra
+Maintainer: David Palmer <palmer@overchat.org>
+Build-Depends: debhelper (>= 7)
+Standards-Version: 3.7.3
+Homepage: http://www.daybologic.co.uk/
+
+Package: libdaybobase-perl
+Architecture: all
+Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}, perl, dl-libconfreader-perl
+Description: Daybo Logic database abstraction layer.
+ Objects relating to the handling of relational DBMS systems.  A layer on top of
+ DBI which can be used for exception handling, persistent DB connections, logging etc.

branches/daybobase-0.2.0/debian/dirs

+opt/daybo/lib

branches/daybobase-0.2.0/debian/rules

+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+
+# Uncomment this to turn on verbose mode.
+export DH_VERBOSE=1
+
+PKG_NAME=libdaybobase-perl
+
+configure: configure-stamp
+configure-stamp:
+	dh_testdir
+	# Add here commands to configure the package.
+
+	touch configure-stamp
+
+
+build: build-stamp
+
+build-stamp: configure-stamp  
+	dh_testdir
+
+	# Add here commands to compile the package.
+	#$(MAKE)
+	mkdir obj/
+	pod2man src/DayboBase.pm | gzip -c > obj/DayboBase.3pm.gz
+
+	touch $@
+
+clean: 
+	dh_testdir
+	dh_testroot
+	rm -f build-stamp configure-stamp
+
+	# Add here commands to clean up after the build process.
+	#$(MAKE) clean
+
+	dh_clean 
+
+install: build
+	dh_testdir
+	dh_testroot
+	dh_clean -k 
+	dh_installdirs
+
+	install -m 755 -d $(CURDIR)/debian/$(PKG_NAME)/opt/daybo/lib
+	install -m 755 -d $(CURDIR)/debian/$(PKG_NAME)/usr/share/man/man3
+	install -m 755 src/DayboBase.pm $(CURDIR)/debian/$(PKG_NAME)/opt/daybo/lib/DayboBase.pm
+	install -m 644 obj/DayboBase.3pm.gz $(CURDIR)/debian/$(PKG_NAME)/usr/share/man/man3/DayboBase.3pm.gz
+
+# Build architecture-independent files here.
+binary-indep: build install
+# We have nothing to do by default.
+
+# Build architecture-dependent files here.
+binary-arch: build install
+	dh_testdir
+	dh_testroot
+#	dh_installchangelogs 
+#	dh_installdocs
+#	dh_installexamples
+	dh_install
+#	dh_installmenu
+#	dh_installdebconf	
+#	dh_installlogrotate
+#	dh_installemacsen
+#	dh_installpam
+#	dh_installmime
+#	dh_python
+#	dh_installinit
+#	dh_installcron
+#	dh_installinfo
+	dh_installman
+#	dh_link
+#	dh_strip
+	dh_compress
+	dh_fixperms
+	dh_perl
+#	dh_makeshlibs
+	dh_installdeb
+	dh_shlibdeps
+	dh_gencontrol
+	dh_md5sums
+	dh_builddeb
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install configure

branches/daybobase-0.2.0/src/DayboBase.pm

+#!/usr/bin/perl -w
+#
+# This is a module which implements Daybo Logic's database access system,
+# I have been meaning to write this properly many times, but never found the
+# time.  It's fairly straightforward, and avoids us re-inventing the wheel
+# over and over again.  Many companies use this standard approach.
+#
+#----------------------------------------------------------------------------
+use strict;
+use warnings;
+
+use lib '/opt/daybo/lib';
+
+use Data::UUID; # For identifying prepared (compiled) statements.
+use DBI;
+use Time::HiRes qw( time );
+use DlConfReader;
+
+package DayboBase;
+use Error qw(:try);
+use Data::Dumper;
+
+use constant DEFAULT_IDENTIFIER => 'database';
+#----------------------------------------------------------------------------
+sub new
+{
+  my @rows = ( ); # Rows currently in memory
+  my %credentials = ( );
+  my %preps = ( );
+  my %prepBindCount = ( );
+  my $proto = $_[0];
+  my $Options = $_[1];
+  my $class = ref($proto) || $proto;
+  my $self = {
+    _rows => \@rows,
+    _last_err_msg => '',
+    _sth => undef, # Current statement handle
+    _dbh => undef,
+    _database => undef, # String identifier of database in configuration
+    _row_idx => 0, # See GetNextRow(), 0 itself is invalid.
+    _preps => \%preps, # Prepared (compiled) statements
+    _lastPrep => undef, # Handle of last statement to be prepared
+    _exceptions => 1,
+    _debug => 0,
+    _credentials => \%credentials,
+    _last_query_speed => 0,
+    _prep_bind_count => \%prepBindCount
+  };
+
+  bless($self, $class);
+  if ( $self ) {
+    my $database = DEFAULT_IDENTIFIER();
+    my $config = undef;
+
+    if ( $Options ) {
+      $database = $Options->{'Database'}
+        if ( defined($Options->{'Database'}) );
+
+      $config = $Options->{'ConfigFile'}
+        if ( defined($Options->{'ConfigFile'}) );
+
+      $self->{'_debug'} = 1 if ( $Options->{'Debug'} );
+    }
+
+    $config = $self->_LocateConfig()
+      unless ( defined($config) );
+
+    $self->_Setup($database, $config);
+    return $self;
+  }
+  die('Unknown error in DayboBase::new()');
+}
+#----------------------------------------------------------------------------
+sub DESTROY
+{
+  my $self = $_[0];
+  my $n;
+  $self->FinishQuery(); # Discard / finish the currently query and all rows.
+  $n = $self->UnprepareAll(); # Discard all prepared (compiled) statements.
+  printf(
+    STDERR
+    "%s::%s(): %u prepared statements remained at object destruction\n",
+    ref($self), 'DESTROY', $n
+  )
+    if ( $self->{'_debug'} );
+  # And finally shut down the connection to prevent a rollback.
+  $self->{'_dbh'}->disconnect() if ( $self->{'_dbh'} );
+
+  return;
+}
+#----------------------------------------------------------------------------
+=item Handle
+
+This function is effectively a synonym for new(), but may differ from new()
+in future versions.  Handle() is the recommended interface.
+
+Input: A hashref of:
+	Database:
+	  An optional parameter indicating which database to use.  By default,
+	  a default database called 'database' is used.  This string defines
+	  which section is read for credentials from the configuration file.
+	ConfigFile:
+	  An optional path to the configuration file for reading credentials.
+	  By default, the name of the process is used ($0), with any extension
+	  stripped, and replaced with '.ini'.  A search path is applied,
+	  /etc/daybologic/conf/ followed by the working directory of the
+	  process.  The configuration file must have very strict permissions,
+	  which are only readable (and optionally writable by the process).
+
+Return:
+	The return value is an object of the class DayboBase.
+	An exception is thrown in case of any problems.
+
+=cut
+#----------------------------------------------------------------------------
+sub Handle
+{
+  my $obj;
+  my @args = ( );
+  $args[0] = $_[0] if ( $_[0] );
+  $obj = new DayboBase(@args);
+  $obj = ( $obj->Ping() ) ? ( $obj ) : ( undef );
+  return $obj;
+}
+#----------------------------------------------------------------------------
+=item Query
+
+This function is execute to execute an ad-hoc query, which has not been
+pre-compiled.  It accepts a variable number of parameters.  The first
+parameter (aside from the hidden object parameter), is the string value of
+the query you wish to execute.  Further parameters are bound variables, eg.
+the parameters which would replace '?' in a compiled statement.
+The result of the function is boolean to indicate whether the statement
+execution was succesful. In case of error, call GetLastErrorMsg() for a
+readable string.
+
+Input:
+	QueryStr:
+	  The query itself in human-readable string form.
+	BoundParams:
+	  One or more variables to replace bound variables within the QueryStr.
+
+Return:
+	Boolean to indicate success or failure.  By default, exceptions are thrown
+	using die(), a failure return value will only happen if you call Exceptions(0).
+
+=cut
+#----------------------------------------------------------------------------
+sub Query
+{
+  my $startT = 0;
+  my $stmtHandle;
+  my $ret = 0;
+  my ( $self, $QueryStr ) = ( shift, shift );
+  my @BoundParams = @_;
+
+  # If statement handle set, flush old query info away
+  $self->FinishQuery() if ( $self->{'_sth'} );
+
+  unless ( $self->{'_dbh'} ) {
+    my $msg = 'Attempt to Query() before database connection was established';
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return $ret;
+  }
+
+  $startT = time();
+  $stmtHandle = $self->Prepare($QueryStr);
+  if ( $stmtHandle ) {
+    $self->Execute($stmtHandle, @BoundParams);
+    $self->Unprepare($stmtHandle);
+    $ret = 1;
+  }
+  $self->LastQuerySpeed(time() - $startT);
+  return $ret;
+}
+#----------------------------------------------------------------------------
+=item Prepare
+
+The Prepare() function prepares a statement to be executed later.  Multiple
+statements may be prepared, a handle, which uniquely identifies that statement
+is returned.  If the statement cannot be compiled, undef is returned.  If exceptions
+are enabled (the default), an exception will be thrown, if the statement cannot be
+prepared.  The return value is suitable for use with Execute().
+
+Input:
+	QueryStr:
+	  The string test of the query you wish to compile.  The statement
+	  may have bindings, which will be handled by Execute() later on.
+
+	Output:
+	  An opaque handle which uniquely identifies the prepared (compiled)
+	  statement.
+
+Return:
+	An opaque statement handle, or undef if the statement preparation
+	was not possible.
+
+=cut
+#----------------------------------------------------------------------------
+sub Prepare($$)
+{
+  my $newHandle = undef;
+  my ( $self, $QueryStr ) = @_;
+  my $prep = undef;
+  my $prepBindCount = 0;
+  my $ug = new Data::UUID;
+
+  unless ( $self->{'_dbh'} ) {
+    my $msg = 'Attempt to Prepare() before database connection was established';
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return $newHandle;
+  }
+
+  $prep = $self->{'_dbh'}->prepare($QueryStr);
+  $prepBindCount = $self->_BindCount($QueryStr);
+  if ( $prep ) {
+    $newHandle = $ug->create(); # Create a new public handle (opaque)
+    die 'Assertion failed' if ( $self->{'_preps'}->{$newHandle} );
+    $self->{'_preps'}->{$newHandle} = \$prep;
+    # Store information about how many bound parameters are in this query
+    $self->{'_prep_bind_count'}->{$newHandle} = $prepBindCount;
+    $self->{'_lastPrep'} = $newHandle;
+    if ( $self->{'_debug'} ) {
+      printf(
+        STDERR
+        "PREPARE: %s: %s\n", $self->_PrintableHandle($newHandle), $QueryStr
+      );
+    }
+  }
+
+  if ( $self->{'_exceptions'} ) {
+    die unless ( $newHandle );
+  }
+  return $newHandle;
+}
+
+#----------------------------------------------------------------------------
+=item Unprepare
+
+The Unprepare() function releases the resources associated with a prepared
+(compiled) SQL expression.  You do not need to use this functionality unless
+you will be preparing many dynamic statements which need to be pre-compiled
+for performance reasons, and you will not be destroying the DaybeBase object.
+
+The Query() function uses Prepare(), Execute() and Unprepare() internally.
+
+Input:
+	StmtHandle:
+	  The statement handle, which is an opaque value returned by
+	  Prepare().  If no value is passed, the last statement to be
+	  prepared, will be unprepared.
+
+Output:
+	A true or false value, true on success, false if the expression
+	did not exist, or was already released.  you do not need to handle
+	this unless you have turned exceptions off.
+
+=cut
+#----------------------------------------------------------------------------
+sub Unprepare($$)
+{
+  my $ret = 0;
+  my ( $self, $StmtHandle ) = @_;
+
+  if ( $self ) {
+    $StmtHandle = $StmtHandle || $self->{'_lastPrep'};
+    if ( exists($self->{'_preps'}->{$StmtHandle}) ) {
+      my $sth = $self->{'_preps'}->{$StmtHandle}; # Fetch the real handle
+      if ( $sth ) {
+        $$sth->finish();
+        $ret = 1;
+        printf(STDERR "UNPREPARE: %s\n", $self->_PrintableHandle($StmtHandle))
+          if ( $self->{'_debug'} );
+      }
+      delete($self->{'_preps'}->{$StmtHandle});
+      delete($self->{'_prep_bind_count'}->{$StmtHandle});
+    }
+  }
+
+  unless ( $ret ) {
+    my $msg = sprintf(
+      'Unable to unprepare statement: %s',
+      $self->_PrintableHandle($StmtHandle)
+    );
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+=item UnprepareAll
+
+This function is called by the destructor and will destroy all prepared
+(compiled) statements.  It has been made available to users, although I am
+not sure if it's wise to actually call this.  You should Unprepare() statements
+as soon as you no longer need them.  This is the lazy man's interface for
+occasional garbage collection if you choose not to do so, for performance
+reasons or whatever else.
+
+Input:
+	No input is expected or accepted.
+
+Return:
+	The number of prepared statements destroyed is returned for
+	your convenience (mainly for printing to any user-defined log etc).
+	undef is returned in case of a failure, or an exception is thrown.
+
+=cut
+#----------------------------------------------------------------------------
+sub UnprepareAll
+{
+  my $n = undef;
+  my $self = $_[0];
+  if ( $self ) {
+    $n = 0;
+    foreach my $stmtHandle ( keys( %{ $self->{'_preps'} } ) ) {
+      $n++ if ( $self->Unprepare($stmtHandle) );
+    }
+  }
+  return $n;
+}
+#----------------------------------------------------------------------------
+=item Execute
+
+The Execute() function executes a previously prepared statement.  If the
+for the compiled statement is not passed, the most previous statement to
+be compiled will be assumed.
+
+Additional parameters after the statement handle will be assumed to be
+bindings.
+
+Input:
+	StmtHandle:
+	  The statement handle returned from Prepare(), or undef for the
+	  most recently prepared statement.
+	BoundParams:
+	  Any number of additional bound variables.  If the number passed
+	  does not match that seen originally by Prepare(), an exception
+	  will be raised, or an error set.
+
+Return:
+	The function returns true if the statement could be executed
+	successfully.  In case of error, a false value is returned,
+	or, if exceptions are enabled, an exception is raised.
+	An error message will typically be set regardless.
+=cut
+#----------------------------------------------------------------------------
+sub Execute
+{
+  my $ret = 0;
+  my ( $self, $StmtHandle ) = ( shift, shift );
+  my @BoundParams = @_;
+  my $prep = undef;
+
+  $StmtHandle = $StmtHandle || $self->{'_lastPrep'};
+  $prep = $self->{'_preps'}->{$StmtHandle} if ( $StmtHandle );
+  if ( $prep ) {
+    my $n;
+    printf(STDERR "EXECUTE: %s\n", $self->_PrintableHandle($StmtHandle))
+      if ( $self->{'_debug'} );
+    return $ret unless ( $self->_CheckParameterCount($StmtHandle, scalar(@BoundParams)) );
+    $self->{'_sth'} = $$prep;
+    $n = $self->{'_sth'}->execute();
+    $n = 0 if ( $n eq '0E0' ); # Override this annoying edge case
+    if ( $n ) { # Any rows affected?
+      my $i = 0;
+      while ( my $ref = $self->{'_sth'}->fetchrow_hashref() ) {
+        my @newRow;
+        if ( $i == 0 ) { # First collect the column names
+          my @newHeadingRow = keys(%$ref);
+          $self->{'_rows'}->[$i] = \@newHeadingRow;
+        }
+        @newRow = values(%$ref);
+        $self->{'_rows'}->[++$i] = \@newRow;
+      }
+    }
+    $ret = 1; # Function success.
+  } else {
+    my $msg =
+      sprintf(
+        'Prepared statement %s not executable',
+        $self->_PrintableHandle($StmtHandle)
+      );
+
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+  }
+
+  return $ret;
+}
+#----------------------------------------------------------------------------
+=item GetLastErrorMsg
+
+This function returns the last error encounterd in a human-readable form.
+There is currently no international language support for this.  The message
+is usually just whatever is returned from the back-end DBI or DBD::mysql
+libraries.
+
+Input:
+	No input is expected or accepted.
+
+Output:
+	A string is returned directly.  If no errors have been encountered,
+	the string is empty, but always printable.
+
+=cut
+#----------------------------------------------------------------------------
+sub GetLastErrorMsg($)
+{
+  my $self = $_[0];
+  return $self->{'_last_err_msg'};
+}
+#----------------------------------------------------------------------------
+=item LastQuerySpeed
+
+The LastQuerySpeed() function returns the time taken to execute the last
+query.  Optionally, the time taken for the query may be passed as a
+parameter.  This is generally for internal use only.
+
+Input:
+	A floating point number, indicating the time taken for
+	the last query.
+
+Return:
+	A floating point number indicating the speed of the last query.
+
+=cut
+#----------------------------------------------------------------------------
+sub LastQuerySpeed
+{
+  my ( $self, $LastTime ) = @_;
+  my $t = $LastTime;
+
+  if ( defined($t) ) {
+    $t = 0 if ( $t < 0 ); # Disallow negative numbers.
+    $self->{'_last_query_time'} = $t;
+  }
+  return $self->{'_last_query_time'};
+}
+#----------------------------------------------------------------------------
+=item FinishQuery
+
+Discards state associated with the most recent query executed.  If you perform
+another Query() or similar call, this state will be flushed anyway.
+
+Input:
+	No input expected or accepted.
+
+Output:
+	No return value.
+
+=cut
+#----------------------------------------------------------------------------
+sub FinishQuery($)
+{
+  my $self = $_[0];
+  if ( $self->{'_sth'} ) {
+    $self->{'_sth'}->finish(); # Clean up internal state via DBI
+    $self->{'_sth'} = undef; # Discard statement handle
+    @{ $self->{'_rows'} } = ( ); # Flush all stored rows.
+    $self->{'_row_idx'} = 0; # Reset row index
+  }
+}
+#----------------------------------------------------------------------------
+=item Exceptions
+
+The Exceptions() function controls the behaviour of the package when a failure
+is encountered, such as a problem accessing the database.  By default, the
+package will call die().  However, where a program must be robust and you
+wish to handle exit codes gracefully, you may want to switch exceptions off.
+This is not recommended.  The recommended behaviour is to handle the exception
+using eval{}.
+
+Mode must be 0 or 1, any other value will cause die() to be called, regardless
+of the current exception state.
+
+Input:
+	Mode:
+	  0: Turn exceptions off
+	  1: Turn exceptions on (Default, currently implemented via die).
+
+Output:
+	The new state is returned (boolean).
+
+=cut
+#----------------------------------------------------------------------------
+sub Exceptions($$)
+{
+  my ( $self, $Mode ) = @_;
+  if ( $self ) {
+    if ( defined($Mode) && $Mode =~ m/^\d+$/ ) {
+      $self->{'_exceptions'} = $Mode if ( $Mode == 0 || $Mode == 1 );
+    }
+    return $self->{'_exceptions'};
+  }
+  die 'Bad call to Exceptions()';
+}
+#----------------------------------------------------------------------------
+=item Ping
+
+The Ping() function ensures that the database is available for the next query,
+and re-establishes the connection where possible.  This function is called
+internally in many locations, so calling another function will often result
+in this function being called.  This function establishes the initial
+connection, internally.
+
+Input:
+	No input is accepted or required.
+
+Output:
+	The function will return a true value if the database connection
+	has been re-established.  An exception is thrown if the database
+	cannot be pinged, unless Exceptions(0) has been called.
+
+=cut
+#----------------------------------------------------------------------------
+sub Ping($)
+{
+  my $self = $_[0];
+  my $ok = 0;
+  if ( $self->{'_dbh'} ) { # Connection established previously?
+    $ok = $self->{'_dbh'}->ping();
+  } else { # Not established yet?
+    my %crd = ( );
+    my $dsn = 'dbi:%s:database=%s;host=%s';
+
+    if ( $self->GetCredentials(\%crd, undef) ) {
+      $dsn = sprintf($dsn, $crd{'type'}, $crd{'db'}, $crd{'host'});
+      $self->{'_dbh'} = DBI->connect(
+        $dsn, $crd{'user'}, $crd{'pass'},
+        { PrintError => 0, AutoCommit => 0, RaiseError => 1 }
+      );
+    } elsif ( !$self->{'_exceptions'} ) {
+      # Failed to get credentials and exceptions not handled
+      return 0;
+    }
+    $ok = 1 if ( $self->{'_dbh'} );
+  }
+
+  unless ( $ok ) {
+    die($DBI::errstr) if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($DBI::err_str);
+    return $ok;
+  }
+  return $ok;
+}
+#----------------------------------------------------------------------------
+=item GetCredentials
+
+The GetCredentials() function is a general purpose database access credentials
+loading function.  It can be used in conjunction with a configuration reader
+object (DlConfReader), or directly once already used.
+
+Input:
+	CredPtr:
+	  User supplied reference to a hash which will be loaded with the
+	  credentials: user, type, pass, host, hostro and db, as they appear
+	  within the configuration file.
+	ReaderPtr:
+	  A reference to the reader (only used once), optional after the
+	  second call.  nb. the internal _Setup() routine which is called
+	  automagically makes the first call, so under almost all circumstances,
+	  you will not need to pass this object, and if you do so, it will be
+	  ignored.
+
+Returns:
+	A true value, a negative value or an exception will be raised should there
+	be a problem.  Addtionally, if the data in the configuration file is incomplete,
+	this is considered a failure within the first call.
+
+=cut
+#----------------------------------------------------------------------------
+sub GetCredentials($$$)
+{
+  my ( $self, $CredPtr, $ReaderPtr ) = @_;
+  my @knownKeys = ( 'host', 'hostro', 'db', 'user', 'pass', 'type' );
+  my @cKeys = keys( %{ $self->{'_credentials'} } );
+  unless ( @cKeys ) { # Credentials not loaded?
+    if ( $ReaderPtr ) { # Reader passed?
+      foreach my $k ( @knownKeys ) {
+        my $d = $$ReaderPtr->GetDatum($self->{'_database'}, $k);
+        if ( defined($d) ) {
+          $self->{'_credentials'}->{$k} = $d; # Save for later.
+        } else {
+          my $msg = sprintf('Missing %s in configuration for [%s]', $k, $self->{'_database'});
+          die($msg . "\n") if ( $self->{'_exceptions'} );
+          $self->_SetLastErrorMsg($msg);
+          return undef;
+        }
+      }
+    } else {
+      my $msg = 'No credentials and reader not available';
+      die($msg . "\n") if ( $self->{'_exceptions'} );
+      $self->_SetLastErrorMsg($msg);
+      return undef;
+    }
+  }
+
+  # The credentials should be loaded now, let's copy them to the caller.
+  if ( $CredPtr ) {
+    foreach my $k ( @cKeys ) {
+      $CredPtr->{$k} = $self->{'_credentials'}->{$k};
+    }
+  }
+
+  return 1;
+}
+#----------------------------------------------------------------------------
+=item Rows
+
+The Rows() function returns the number of rows associated with the currently
+executing statement.  If no statement is currently executing, undef is returned,
+however, if exceptions are enabled, an exception is raised.
+
+Input:
+	No input is expected or accepted.
+
+Output:
+	The number of rows, or undef
+
+=cut
+#----------------------------------------------------------------------------
+sub Rows($)
+{
+  my $rows = undef;
+  my $self = $_[0];
+
+  if ( $self ) {
+    unless ( $self->{'_dbh'} ) {
+      my $msg = 'Attempt to call Rows() before database connection was established';
+      die($msg . "\n") if ( $self->{'_exceptions'} );
+      $self->_SetLastErrorMsg($msg);
+      return $rows;
+    }
+    unless ( $self->{'_sth'} ) {
+      my $msg = 'Call to Rows() when no query is currently active';
+      die($msg . "\n") if ( $self->{'_exceptions'} );
+      $self->_SetLastErrorMsg($msg);
+      return $rows;
+    }
+    $rows = $self->{'_sth'}->rows();
+  } else {
+    die("No DayboBase object\n");
+  }
+  return $rows;
+}
+#----------------------------------------------------------------------------
+=item GetNextRow
+
+The GetNextRow() function returns the data for the next row pertaining to
+the active query in progress.  The hashref may be optionally passed by the
+caller, which is more in keeping with traditional memory management, and it
+compatible with the C or C++ variants of this code (not yet released).
+
+The hash will be populated with keys which match the column names,
+and values which match the data.  No attempt is made to ascertain the
+types of the data at time of writing, so you must treat the data with
+caution and consider it tainted.
+
+Input:
+	RowDataRef:
+	  An optional reference to a hash the caller supplies which we
+	  will populate with column names and data.
+
+Return:
+	The RowDataRef, or a reference to an internally generated hash.
+
+=cut
+#----------------------------------------------------------------------------
+sub GetNextRow($$)
+{
+  my $F = 'GetNextRow';
+  my $numRows;
+  my $c = 0; # Column count
+  my %surrogate = ( ); # Use if caller doesn't pass RowDataRef
+  my $ret = undef; # Return value
+  my ( $self, $RowDataRef ) = @_; # Optional, but self is mandatory.
+  $RowDataRef = \%surrogate unless ( $RowDataRef ); # Backup plan
+
+  # Make sure the database handle is valid
+  unless ( $self->{'_dbh'} ) {
+    my $msg = sprintf('Attempt to %s() whilst no DB handle', $F);
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return $ret;
+  }
+
+  # Make sure there is currently a statement executing.
+  unless ( $self->{'_sth'} ) {
+    my $msg = sprintf('Attempt to %s() whilst no active statement', $F);
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return $ret;
+  }
+
+  $c = scalar(@{ $self->{'_rows'}->[0] }); # Get number of columns.
+  $numRows = scalar ( @{ $self->{'_rows'} } ); # Get number of rows.
+
+  if ( $self->{'_row_idx'}+1 < $numRows ) {
+    $self->{'_row_idx'}++;
+    $ret = $RowDataRef;
+    for ( my $i = 0; $i < $c; $i++ ) {
+      $RowDataRef->{$self->{'_rows'}->[0]->[$i]} =
+        $self->{'_rows'}->[$self->{'_row_idx'}]->[$i];
+    }
+  }
+  print(
+    STDERR sprintf(
+      '%s::%s(%u): %s :- %s',
+      ref($self), $F, $self->{'_row_idx'},
+      'Out of bounds', Dumper $RowDataRef
+    )
+  ) if ( $self->{'_debug'} );
+
+  return $ret;
+}
+#----------------------------------------------------------------------------
+=item ValidateStatementHandle
+
+This function will return 1 if the handle supplied is valid for execution.
+0 otherwise.  The function does not throw exceptions in normal circumstances.
+
+Input:
+	StmtHandle:
+	  The opaque handle returned by the Prepare() function.  If no value
+	  is passed, the last prepared statement is assumed, which is probably
+	  not what you want, if you want to use this function.
+
+Return:
+	A boolean true or false value is returned.
+
+=cut
+#----------------------------------------------------------------------------
+sub ValidateStatementHandle($$)
+{
+  my $ret = 0;
+  my ( $self, $StmtHandle ) = @_;
+  if ( $self ) {
+    if ( exists($self->{'_preps'}->{$StmtHandle}) ) {
+      $ret = 1 if ( $self->{'_preps'}->{$StmtHandle} );
+    }
+  }
+
+  if ( $self && $self->{'_debug'} ) {
+    printf(
+      STDERR "%s::%s(\'%s\'): %u\n",
+      ref($self), 'ValidateStatementHandle',
+      $self->_PrintableHandle($StmtHandle), $ret
+    );
+  }
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub _Setup($$$)
+{
+  my $ret = 0;
+  my $cnfReader = undef;
+  my ( $self, $Database, $ConfigFile ) = @_;
+  die 'Internal error' unless ( $self );
+
+  # First check for automatic search error
+  unless ( $ConfigFile ) {
+    my $msg = 'Cannot locate config file';
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return 0;
+  }
+
+  # Now check whether we can actually read the file
+  $cnfReader = DlConfReader->new();
+  if ( $cnfReader ) {
+    if ( $cnfReader->SetFn($ConfigFile) ) {
+      my $err = undef;
+      try {
+        $cnfReader->Reload();
+      }
+      catch Error with {
+        $err = shift;
+      };
+      if ( defined($err) ) {
+        die($err) if ( $self->{'_exceptions'} );
+        $self->_SetLastErrorMsg($err);
+        return 0;
+      }
+    } else {
+      my $msg = sprintf('Error setting/illegal config file name: %s', $ConfigFile);
+      die($msg . "\n") if ( $self->{'_exceptions'} );
+      $self->_SetLastErrorMsg($msg);
+      return 0;
+    }
+  } else {
+    my $msg = 'Error creating DlConfReader object';
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return 0;
+  }
+
+  $self->{'_database'} = $Database;
+  $ret = $self->GetCredentials(undef, \$cnfReader);
+  $self->{'_database'} = undef unless ( $ret );
+  return $ret;
+}
+#----------------------------------------------------------------------------
+sub _LocateConfig($)
+{
+  my $self = $_[0];
+  my $funcName = '_LocateConfig';
+  my @fnParts;
+  my $iniResult = undef;
+  my @searchPath = ( '/etc/daybologic/conf', '.' );
+  my $cnfFile = $0;
+
+  printf(STDERR "%s(): Script name: %s\n", $funcName, $cnfFile)
+    if ( $self->{'_debug'} );
+  @fnParts = split(m/\./, $cnfFile);
+  pop(@fnParts) if ( scalar(@fnParts) > 1 );
+  $cnfFile = join('.', @fnParts);
+  $cnfFile .= '.ini'; # Append configuration file extension
+  printf(STDERR "%s(): Config name: %s\n", $funcName, $cnfFile)
+    if ( $self->{'_debug'} );
+
+  foreach my $ini ( @searchPath ) {
+    $ini .= '/' . $cnfFile;
+    if ( -f $ini ) {
+      $iniResult = $ini;
+      last;
+    }
+  }
+  return $iniResult;
+}
+#----------------------------------------------------------------------------
+sub _PrintableHandle($$)
+{
+  my $str = '00000000-0000-0000-0000-000000000000';
+  my ( $self, $Hdle ) = @_;
+  if ( defined($Hdle) ) {
+    my $ug = new Data::UUID();
+    $str = $ug->to_string($Hdle);
+  }
+  return '{' . $str . '}';
+}
+#----------------------------------------------------------------------------
+sub _BindCount($$)
+{
+  my $bindCount = 0;
+  my ( $self, $QueryStr ) = @_;
+  if ( $QueryStr ) {
+    my $quoted = 0;
+    my @chars = split(//, $QueryStr);
+    foreach my $c ( @chars ) {
+      if ( $c eq "\'" ) {
+        $quoted = (!$quoted);
+        next;
+      }
+      $bindCount++ if ( $c eq '?' );
+    }
+  }
+  printf(
+    STDERR "%s::%s(\'%s\'): %u\n",
+    ref($self), '_BindCount', $QueryStr, $bindCount
+  ) if ( $self->{'_debug'} );
+  return $bindCount;
+}
+#----------------------------------------------------------------------------
+sub _CheckParameterCount($$$)
+{
+  my $expectCount;
+  my ( $self, $StmtHandle, $Count ) = @_;
+  if ( $self->{'_debug'} ) {
+    die 'Assertion failure, no self reference' unless ( $self );
+    die 'Assertion failure, Count is not numeric' unless ( $Count =~ m/^\d+$/ );
+    die 'Assertion failure, null statement handle' unless ( $StmtHandle );
+    die unless ( exists($self->{'_prep_bind_count'}->{$StmtHandle}) );
+  }
+
+  $expectCount = $self->{'_prep_bind_count'}->{$StmtHandle} || 0;
+  if ( $expectCount != $Count ) {
+    my $msg = sprintf(
+      'Bound parameters expected %u, passed %u',
+      $expectCount, $Count
+    );
+    die($msg . "\n") if ( $self->{'_exceptions'} );
+    $self->_SetLastErrorMsg($msg);
+    return 0;
+  }
+  return 1;
+}
+#----------------------------------------------------------------------------
+sub _SetLastErrorMsg($$)
+{
+  my ( $self, $ErrorMsg ) = @_;
+  $self->{'_last_err_msg'} = $ErrorMsg;
+  printf(
+    STDERR "%s::%s(): \"%s\"\n",
+    ref($self), '_SetLastErrorMsg', $ErrorMsg
+  ) if ( $self->{'_debug'} );
+
+  return $self;
+}
+#----------------------------------------------------------------------------
+1;