What the plans for PdoQuery have turned into...

This commit is contained in:
Andrew McMillan 2009-10-26 00:09:31 +13:00
parent ba4792e38d
commit 6fee9fcb02
4 changed files with 883 additions and 488 deletions

293
inc/AwlDBDialect.php Normal file
View File

@ -0,0 +1,293 @@
<?php
/**
* AwlDatabase - support for different SQL dialects
*
* This subpackage provides dialect specific support for PostgreSQL, and
* may, over time, be extended to provide support for other SQL dialects.
*
* See http://wiki.davical.org/w/Coding/PdoDatabase for design and usage information.
*
* @package awl
* @subpackage AwlDatabase
* @author Andrew McMillan <andrew@morphoss.com>
* @copyright Morphoss Ltd
* @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @compatibility Requires PHP 5.1 or later
*/
/**
* The AwlDBDialect class handles
* @package awl
*/
class AwlDBDialect {
/**#@+
* @access private
*/
/**
* Holds the name of the database dialect
*/
protected $dialect;
/**
* Holds the PDO database connection
*/
protected $db;
/**
* Holds the version
*/
private $version;
/**#@-*/
/**
* Parses the connection string to ascertain the database dialect. Returns true if the dialect is supported
* and fails if the dialect is not supported. All code to support any given database should be within in an
* external include.
*
* The database will be opened.
*
* @param string $connection_string The PDO connection string, in all it's glory
* @param string $dbuser The database username to connect as
* @param string $dbpass The database password to connect with
* @param array $options An array of driver options
*/
function __construct( $connection_string, $dbuser=null, $dbpass=null, $options=null ) {
if ( preg_match( '/^(pgsql):/', $connection_string, $matches ) ) {
$this->dialect = $matches[1];
}
else {
trigger_error("Unsupported database connection '".$connection_string."'", E_USER_ERROR);
}
$this->db = new PDO( $connection_string, $dbuser, $dbpass, $options );
}
/**
* Sets the current search path for the database.
*/
function SetSearchPath( $search_path = null ) {
if ( !isset($this->dialect) ) {
trigger_error("Unsupported database dialect", E_USER_ERROR);
}
switch ( $this->dialect ) {
case 'pgsql':
if ( $search_path == null ) $search_path = 'public';
$sql = "SET search_path TO " . $this->Quote( $search_path, 'identifier' );
return $sql;
}
}
/**
* Sets the current search path for the database.
* @param handle $pdo A handle to an opened database
*/
function GetVersion( ) {
if ( isset($this->version) ) return $this->version;
if ( !isset($this->dialect) ) {
trigger_error("Unsupported database dialect", E_USER_ERROR);
}
$version = $this->dialect.':';
switch ( $this->dialect ) {
case 'pgsql':
$sql = "SELECT version()";
if ( $sth = $this->db->query($sql) ) {
$row = $sth->fetch(PDO::FETCH_NUM);
$version .= preg_replace( '/^PostgreSQL (\d+\.\d+)\..*$/i', '$1', $row[0]);
}
break;
}
$this->version = $version;
return $version;
}
/**
* Returns the SQL for the current database dialect which will return a two-column resultset containing a
* list of fields and their associated data types.
* @param string $tablename_string The name of the table we want fields from
*/
function GetFields( $tablename_string ) {
if ( !isset($this->dialect) ) {
trigger_error("Unsupported database dialect", E_USER_ERROR);
}
switch ( $this->dialect ) {
case 'pgsql':
$tablename_string = $this->Quote($tablename_string, 'identifier');
$sql = "SELECT f.attname, t.typname FROM pg_attribute f ";
$sql .= "JOIN pg_class c ON ( f.attrelid = c.oid ) ";
$sql .= "JOIN pg_type t ON ( f.atttypid = t.oid ) ";
$sql .= "WHERE relname = $tablename_string AND attnum >= 0 order by f.attnum;";
return $sql;
}
}
/**
* Translates the given SQL string into a form that will hopefully work for this database dialect. This hook
* is intended to be used by developers to provide support for differences in database operation by translating
* the query string in an arbitrary way, such as through a file or database lookup.
*
* The actual translation to other SQL dialects will be application-specific, so that any routines
* called by this will be external to this library, or will use resources loaded from some source
* external to this library.
*
* The application developer is expected to use this functionality to solve harder translation problems,
* but is less likely to call this directly, hopefully switching ->Prepare to ->PrepareTranslated in those
* cases, and then adding that statement to whatever SQL translation infrastructure is in place.
*/
function TranslateSQL( $sql_string ) {
// Noop for the time being...
return $sql_string;
}
/**
* Returns $value escaped in an appropriate way for this database dialect.
* @param mixed $value The value to be escaped
* @param string $value_type The type of escaping desired. If blank this will be worked out from gettype($value). The special
* type of 'identifier' can also be used for escaping of SQL identifiers.
*/
function Quote( $value, $value_type = null ) {
if ( isset($value_type) && $value_type == 'identifier' ) {
if ( $this->dialect == 'mysql' ) {
/** TODO: Someone should confirm this is correct for MySql */
$rv = '`' . str_replace('`', '\\`', $value ) . '`';
}
else {
$rv = '"' . str_replace('"', '\\"', $value ) . '"';
}
return $rv;
}
switch ( $this->dialect ) {
case 'mysql':
case 'pgsql':
case 'sqlite':
if ( is_string($value_type) ) {
switch( $value_type ) {
case 'null':
$value_type = PDO::PARAM_NULL;
break;
case 'integer':
case 'double' :
$value_type = PDO::PARAM_INT;
break;
case 'boolean':
$value_type = PDO::PARAM_BOOL;
break;
case 'string':
$value_type = PDO::PARAM_STR;
break;
}
}
$rv = $this->db->quote($value);
break;
default:
if ( !isset($value_type) ) {
$value_type = gettype($value);
}
switch ( $value_type ) {
case PDO::PARAM_NULL:
case 'null':
$rv = 'NULL';
break;
case PDO::PARAM_INT:
case 'integer':
case 'double' :
return $str;
case PDO::PARAM_BOOL:
case 'boolean':
$rv = $str ? 'TRUE' : 'FALSE';
break;
case PDO::PARAM_STR:
case 'string':
default:
$str = str_replace("'", "''", $str);
if ( strpos( $str, '\\' ) !== false ) {
$str = str_replace('\\', '\\\\', $str);
if ( $this->dialect == 'pgsql' ) {
/** PostgreSQL wants to know when a string might contain escapes */
$rv = "E'$str'";
}
}
}
break;
}
return $rv;
}
/**
* Replaces query parameters with appropriately escaped substitutions.
*
* The function takes a variable number of arguments, the first is the
* SQL string, with replaceable '?' characters (a la DBI). The subsequent
* parameters being the values to replace into the SQL string.
*
* The values passed to the routine are analyzed for type, and quoted if
* they appear to need quoting. This can go wrong for (e.g.) NULL or
* other special SQL values which are not straightforwardly identifiable
* as needing quoting (or not). In such cases the parameter can be forced
* to be inserted unquoted by passing it as "array( 'plain' => $param )".
*
* @param string The query string with replacable '?' characters.
* @param mixed The values to replace into the SQL string.
* @return The built query string
*/
function ReplaceParameters() {
$argc = func_num_args();
$qry = func_get_arg(0);
$args = func_get_args();
if ( is_array($qry) ) {
/**
* If the first argument is an array we treat that as our arguments instead
*/
$qry = $args[0][0];
$args = $args[0];
$argc = count($args);
}
/**
* We only split into a maximum of $argc chunks. Any leftover ? will remain in
* the string and may be replaced at Exec rather than Prepare.
*/
$parts = explode( '?', $qry, $argc );
$querystring = $parts[0];
$z = count($parts);
for( $i = 1; $i < $z; $i++ ) {
$arg = $args[$i];
if ( !isset($arg) ) {
$querystring .= 'NULL';
}
elseif ( is_array($arg) && $arg['plain'] != '' ) {
// We abuse this, but people should access it through the PgQuery::Plain($v) function
$querystring .= $arg['plain'];
}
else {
$querystring .= $this->Quote($arg); //parameter
}
$querystring .= $parts[$i]; //extras eg. ","
}
if ( isset($parts[$z]) ) $querystring .= $parts[$z]; //puts last part on the end
return $querystring;
}
}

152
inc/AwlDatabase.php Normal file
View File

@ -0,0 +1,152 @@
<?php
/**
* AwlDatabase query/statement class and associated functions
*
* This subpackage provides some functions that are useful around database
* activity and a AwlDialect, AwlDatabase and AwlStatement classes to simplify
* handling of database queries and provide some access for a limited
* ability to handle varying database dialects.
*
* The class is intended to be a very lightweight wrapper with some features
* that have proved useful in developing and debugging web-based applications:
* - All queries are timed, and an expected time can be provided.
* - Parameters replaced into the SQL will be escaped correctly in order to
* minimise the chances of SQL injection errors.
* - Queries which fail, or which exceed their expected execution time, will
* be logged for potential further analysis.
* - Debug logging of queries may be enabled globally, or restricted to
* particular sets of queries.
* - Simple syntax for iterating through a result set.
*
* See http://wiki.davical.org/w/AwlDatabase for design and usage information.
*
* If not already connected, AwlDatabase will attempt to connect to the database,
* successively applying connection parameters from the array in $c->pdo_connect.
*
* We will die if the database is not currently connected and we fail to find
* a working connection.
*
* @package awl
* @subpackage AwlDatabase
* @author Andrew McMillan <andrew@morphoss.com>
* @copyright Morphoss Ltd
* @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @compatibility Requires PHP 5.1 or later
*/
if ( !class_exists('AwlDBDialect') ) require('AwlDBDialect.php');
/**
* Methods in the AwlDBDialect class which we inherit, include:
* __construct()
* SetSearchPath( $search_path )
* GetVersion()
* GetFields( $tablename_string )
* TranslateSQL( $sql_string )
* Quote( $value, $value_type = null )
* ReplaceParameters( $query_string [, param [, ...]] )
*/
/**
* Typically there will only be a single instance of the database level class in an application.
* @package awl
*/
class AwlDatabase extends AwlDBDialect {
/**#@+
* @access private
*/
/**
* Holds the state of the transaction 0 = not started, 1 = in progress, -1 = error pending rollback/commit
*/
protected $txnstate = 0;
/**#@-*/
/**
* Returns a PDOStatement object created using this database, the supplied SQL string, and any parameters given.
* @param string $sql_query_string The SQL string containing optional variable replacements
* @param array $driver_options PDO driver options to the prepare statement, commonly to do with cursors
*/
function prepare( $statement, $driver_options = array() ) {
return $this->db->prepare( $statement, $driver_options );
}
/**
* Returns a PDOStatement object created using this database, the supplied SQL string, and any parameters given.
* @param string $sql_query_string The SQL string containing optional variable replacements
* @param mixed ... Subsequent arguments are positionally replaced into the $sql_query_string
*/
function query( $statement ) {
return $this->db->query( $statement );
}
/**
* Begin a transaction.
*/
function Begin() {
if ( $this->txnstate == 0 ) {
$this->db->beginTransaction();
$this->txnstate = 1;
}
else {
trigger_error("Cannot begin a transaction while a transaction is already active.", E_USER_ERROR);
}
}
/**
* Complete a transaction.
*/
function Commit() {
if ( $this->txnstate != 0 ) {
$this->db->commit();
$this->txnstate = 0;
}
}
/**
* Cancel a transaction in progress.
*/
function Rollback() {
if ( $this->txnstate != 0 ) {
$this->db->rollBack();
$this->txnstate = 0;
}
else {
trigger_error("Cannot rollback unless a transaction is already active.", E_USER_ERROR);
}
}
/**
* Returns the current state of a transaction, indicating if we have begun a transaction, whether the transaction
* has failed, or if we are not in a transaction.
*/
function TransactionState() {
return $this->txnstate;
}
/**
* Operates identically to AwlDatabase::Prepare, except that $this->Translate() will be called on the query
* before any processing.
*/
function PrepareTranslated() {
}
/**
* Switches on or off the processing flag controlling whether subsequent calls to AwlDatabase::Prepare are translated
* as if PrepareTranslated() had been called.
*/
function TranslateAll( $onoff_boolean ) {
}
}

438
inc/AwlQuery.php Normal file
View File

@ -0,0 +1,438 @@
<?php
/**
* @package awl
* @subpackage AWLDB
* @author Andrew McMillan <andrew@morphoss.com>
* @copyright Morphoss Ltd
* @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @compatibility Requires PHP 5.1 or later
*/
require_once('AwlDatabase.php');
/**
* Database query class and associated functions
*
* This subpackage provides some functions that are useful around database
* activity and an AwlQuery class to simplify handling of database queries.
*
* The class is intended to be a very lightweight wrapper with no pretentions
* towards database independence, but it does include some features that have
* proved useful in developing and debugging web-based applications:
* - All queries are timed, and an expected time can be provided.
* - Parameters replaced into the SQL will be escaped correctly in order to
* minimise the chances of SQL injection errors.
* - Queries which fail, or which exceed their expected execution time, will
* be logged for potential further analysis.
* - Debug logging of queries may be enabled globally, or restricted to
* particular sets of queries.
* - Simple syntax for iterating through a result set.
*
* This class is intended as a transitional mechanism for moving from the
* PostgreSQL-specific PgQuery class to something which uses PDO in a more
* replaceable manner.
*
*/
/**
* Connect to the database defined in the $c->db_connect[] (or $c->pg_connect) arrays
*/
function _awl_connect_configured_database() {
global $c, $_awl_dbconn;
/**
* Attempt to connect to the configured connect strings
*/
$_awl_dbconn = false;
if ( isset($c->db_connect) ) {
$connection_strings = $c->db_connect;
}
elseif ( isset($c->pg_connect) ) {
$connection_strings = $c->pg_connect;
}
foreach( $connection_strings AS $k => $v ) {
$dbuser = null;
$dbpass = null;
if ( is_array($v) ) {
$dsn = $v['dsn'];
if ( isset($v['dbuser']) ) $dbuser = $v['dbuser'];
if ( isset($v['dbpass']) ) $dbpass = $v['dbpass'];
}
elseif ( preg_match( '/^(\S+:)?(.*)( user=(\S+))?( password=(\S+))?$/', $v, $matches ) ) {
$dsn = $matches[2];
if ( isset($matches[1]) && $matches[1] != '' ) {
$dsn = $matches[1] . $dsn;
}
else {
$dsn = 'pgsql:' . $dsn;
}
if ( isset($matches[4]) && $matches[4] != '' ) $dbuser = $matches[4];
if ( isset($matches[6]) && $matches[6] != '' ) $dbpass = $matches[6];
}
if ( $_awl_dbconn = new AwlDatabase( $dsn, $dbuser, $dbpass ) ) break;
}
if ( ! $_awl_dbconn ) {
echo <<<EOERRMSG
<html><head><title>Database Connection Failure</title></head><body>
<h1>Database Error</h1>
<h3>Could not connect to database</h3>
</body>
</html>
EOERRMSG;
exit;
}
if ( isset($c->db_schema) && $c->db_schema != '' ) {
$_awl_dbconn->SetSearchPath( $c->db_schema . ',public' );
}
$c->_awl_dbversion = $_awl_dbconn->GetVersion();
}
if ( !function_exists('duration') ) {
/**
* A duration (in decimal seconds) between two times which are the result of calls to microtime()
*
* This simple function is used by the AwlQuery class because the
* microtime function doesn't return a decimal time, so a simple
* subtraction is not sufficient.
*
* @param microtime $t1 start time
* @param microtime $t2 end time
* @return double difference
*/
function duration( $t1, $t2 ) {
list ( $ms1, $s1 ) = explode ( " ", $t1 ); // Format times - by spliting seconds and microseconds
list ( $ms2, $s2 ) = explode ( " ", $t2 );
$s1 = $s2 - $s1;
$s1 = $s1 + ( $ms2 -$ms1 );
return $s1; // Return duration of time
}
}
/**
* The AwlQuery Class.
*
* This class builds and executes SQL Queries and traverses the
* set of results returned from the query.
*
* <b>Example usage</b>
* <code>
* $sql = "SELECT * FROM mytable WHERE mytype = ?";
* $qry = new AwlQuery( $sql, $myunsanitisedtype );
* if ( $qry->Exec("typeselect", __line__, __file__ )
* && $qry->rows > 0 )
* {
* while( $row = $qry->Fetch() ) {
* do_something_with($row);
* }
* }
* </code>
*
* @package awl
*/
class AwlQuery
{
/**#@+
* @access private
*/
/**
* Our database connection, normally copied from a global one
* @var resource
*/
protected $connection;
/**
* The original query string
* @var string
*/
protected $querystring;
/**
* The current array of bound parameters
* @var array
*/
protected $bound_parameters;
/**
* The PDO statement handle, or null if we don't have one yet.
* @var string
*/
protected $sth;
/**
* Result of the last execution
* @var resource
*/
protected $result;
/**
* number of current row - use accessor to get/set
* @var int
*/
protected $rownum = null;
/**
* number of rows from pg_numrows - use accessor to get value
* @var int
*/
protected $rows;
/**
* The Database error information, if the query fails.
* @var string
*/
protected $error_info;
/**
* Stores the query execution time - used to deal with long queries.
* should be read-only
* @var string
*/
protected $execution_time;
/**#@-*/
/**#@+
* @access public
*/
/**
* Where we called this query from so we can find it in our code!
* Debugging may also be selectively enabled for a $location.
* @var string
*/
public $location;
/**
* How long the query should take before a warning is issued.
*
* This is writable, but a method to set it might be a better interface.
* The default is 0.3 seconds.
* @var double
*/
public $query_time_warning = 0.3;
/**#@-*/
/**
* Constructor
* @param string The query string in PDO syntax with replacable '?' characters or bindable parameters.
* @param mixed The values to replace into the SQL string.
* @return The AwlQuery object
*/
function __construct() {
global $_awl_dbconn;
$this->rows = null;
$this->execution_time = 0;
$this->error_info = null;
$this->rownum = -1;
if ( isset($dbconn) ) $this->connection = $_awl_dbconn;
else $this->connection = null;
$argc = func_num_args();
$this->querystring = func_get_arg(0);
if ( 1 < $argc ) {
$args = func_get_args();
array_shift($args);
$this->Bind($args);
}
return $this;
}
/**
* Use a different database connection for this query
* @param resource $new_connection The database connection to use.
*/
function SetConnection( $new_connection ) {
$this->connection = $new_connection;
}
/**
* Log query, optionally with file and line location of the caller.
*
* This function should not really be used outside of AwlQuery. For a more
* useful generic logging interface consider calling dbg_error_log(...);
*
* @param string $locn A string identifying the calling location.
* @param string $tag A tag string, e.g. identifying the type of event.
* @param string $string The information to be logged.
* @param int $line The line number where the logged event occurred.
* @param string $file The file name where the logged event occurred.
*/
function _log_query( $locn, $tag, $string, $line = 0, $file = "") {
// replace more than one space with one space
$string = preg_replace('/\s+/', ' ', $string);
if ( ($tag == 'QF' || $tag == 'SQ') && ( $line != 0 && $file != "" ) ) {
dbg_error_log( "LOG-$locn", " Query: %s: %s in '%s' on line %d", ($tag == 'QF' ? 'Error' : 'Possible slow query'), $tag, $file, $line );
}
while( strlen( $string ) > 0 ) {
dbg_error_log( "LOG-$locn", " Query: %s: %s", $tag, substr( $string, 0, 240) );
$string = substr( "$string", 240 );
}
}
/**
* Quote the given string so it can be safely used within string delimiters
* in a query. To be avoided, in general.
*
* @param mixed $str Data to be converted to a string suitable for including as a value in SQL.
* @return string NULL, TRUE, FALSE, a plain number, or the original string quoted and with ' and \ characters escaped
*/
function quote($str = null) {
if ( !isset($this->connection) ) {
_awl_connect_configured_database();
$this->connection = $GLOBALS['_awl_dbconn'];
}
return $this->connection->Quote($str);
}
/**
* Bind some parameters
*/
function Bind() {
$args = func_get_args();
if ( gettype($args[0]) == 'array' ) {
$this->bound_parameters = $args[0];
/** @TODO: perhaps we should WARN here if there is more than 1 argument */
}
else {
$this->bound_parameters = $args;
}
}
/**
* Tell the database to prepare the query that we will execute
*/
function Prepare() {
if ( !isset($this->connection) ) {
_awl_connect_configured_database();
$this->connection = $GLOBALS['_awl_dbconn'];
}
$this->sth = $this->connection->prepare( $this->querystring );
if ( ! $this->sth ) {
$this->error_info = $this->connection->errorInfo();
}
else $this->error_info = null;
}
/**
* Execute the query, logging any debugging.
*
* <b>Example</b>
* So that you can nicely enable/disable the queries for a particular class, you
* could use some of PHPs magic constants in your call.
* <code>
* $qry->Exec(__CLASS__, __LINE__, __FILE__);
* </code>
*
*
* @param string $location The name of the location for enabling debugging or just
* to help our children find the source of a problem.
* @param int $line The line number where Exec was called
* @param string $file The file where Exec was called
* @return resource The actual result of the query (FWIW)
*/
function Exec( $location = '', $line = 0, $file = '' ) {
global $debuggroups, $c;
$this->location = trim($location);
if ( $this->location == "" ) $this->location = substr($_SERVER['PHP_SELF'],1);
if ( isset($debuggroups['querystring']) || isset($c->dbg['querystring']) || isset($c->dbg['ALL']) ) {
$this->_log_query( $this->location, 'DBGQ', $this->querystring, $line, $file );
}
if ( isset($this->bound_parameters) && !isset($this->sth) ) {
$this->Prepare();
}
$t1 = microtime(true); // get start time
if ( isset($this->sth) && $this->sth !== false ) {
if ( ! $this->sth->execute( $this->bound_parameters ) ) {
$this->error_info = $this->sth->errorInfo();
}
else $this->error_info = null;
}
else if ( $this->sth !== false ) {
/** Ensure we have a connection to the database */
if ( !isset($this->connection) ) {
_awl_connect_configured_database();
$this->connection = $GLOBALS['_awl_dbconn'];
}
$this->sth = $this->connection->query( $this->querystring );
if ( ! $this->sth ) {
$this->error_info = $this->connection->errorInfo();
}
else $this->error_info = null;
}
$success = !isset($this->error_info);
$this->rows = $this->sth->rowCount();
$t2 = microtime(true); // get end time
$i_took = $t2 - $t1;
$c->total_query_time += $i_took;
$this->execution_time = sprintf( "%2.06lf", $i_took);
if ( ! $success ) {
// query failed
$this->errorstring = sprintf( 'SQL error "%s" - %s"', $this->error_info[0], $this->error_info[2]);
$this->_log_query( $this->location, 'QF', $this->errorstring, $line, $file );
}
elseif ( $this->execution_time > $this->query_time_warning ) {
// if execution time is too long
$this->_log_query( $this->location, 'SQ', "Took: $this->execution_time for $this->querystring", $line, $file ); // SQ == Slow Query :-)
}
elseif ( isset($debuggroups[$this->location]) || isset($c->dbg[strtolower($this->location)]) || isset($c->dbg['ALL']) ) {
// query successful, but we're debugging and want to know how long it took anyway
$this->_log_query( $this->location, 'DBGQ', "Took: $this->execution_time for $this->querystring to find $this->rows rows.", $line, $file );
}
return $success;
}
/**
* Fetch the next row from the query results
* @param boolean $as_array True if thing to be returned is array
* @return mixed query row
*/
function Fetch($as_array = false) {
global $c, $debuggroups;
if ( ( isset($debuggroups["$this->location"]) && $debuggroups["$this->location"] > 2 )
|| (isset($c) && is_object($c) && ( isset($c->dbg[strtolower($this->location)]) && isset($c->dbg[strtolower($this->location)]) )
|| isset($c->dbg['ALL']) ) ) {
$this->_log_query( $this->location, "Fetch", "$this->result Rows: $this->rows, Rownum: $this->rownum");
}
if ( ! $this->sth || $this->rows == 0 ) return false; // no results
if ( $this->rownum == null ) $this->rownum = -1;
if ( ($this->rownum + 1) >= $this->rows ) return false; // reached the end of results
$this->rownum++;
if ( isset($debuggroups["$this->location"]) && $debuggroups["$this->location"] > 1 ) {
$this->_log_query( $this->location, "Fetch", "Fetching row $this->rownum" );
}
$row = $this->sth->fetch( ($as_array ? PDO::FETCH_NUM : PDO::FETCH_OBJ) );
return $row;
}
}

View File

@ -1,488 +0,0 @@
<?php
/**
* PDO query class and associated functions
*
* This subpackage provides some functions that are useful around database
* activity and a PdoDialect, PdoDatabase and PdoQuery classes to simplify
* handling of database queries and provide some access for a limited
* ability to handle varying database dialects.
*
* The class is intended to be a very lightweight wrapper with some features
* that have proved useful in developing and debugging web-based applications:
* - All queries are timed, and an expected time can be provided.
* - Parameters replaced into the SQL will be escaped correctly in order to
* minimise the chances of SQL injection errors.
* - Queries which fail, or which exceed their expected execution time, will
* be logged for potential further analysis.
* - Debug logging of queries may be enabled globally, or restricted to
* particular sets of queries.
* - Simple syntax for iterating through a result set.
*
* See http://wiki.davical.org/w/PdoQuery for design and usage information.
*
* If not already connected, PdoQuery will attempt to connect to the database,
* successively applying connection parameters from the array in $c->pdo_connect.
*
* We will die if the database is not currently connected and we fail to find
* a working connection.
*
* @package awl
* @subpackage PdoQuery
* @author Andrew McMillan <andrew@morphoss.com>
* @copyright Morphoss Ltd
* @license http://gnu.org/copyleft/gpl.html GNU GPL v3
* @compatibility Requires PHP 5.1 or later
*/
/**
* The PdoDialect class handles
* @package awl
*/
class PdoDialect {
/**#@+
* @access private
*/
/**
* Holds the name of the database dialect
*/
protected $dialect;
/**#@-*/
/**
* Parses the connection string to ascertain the database dialect. Returns true if the dialect is supported
* and fails if the dialect is not supported. All code to support any given database should be within in an
* external include.
* @param string $connection_string The full PDO connection string
*/
function __construct( $connection_string ) {
if ( preg_match( '/^(pgsql):/', $connection_string, $matches ) ) {
$this->dialect = $matches[1];
}
else {
trigger_error("Unsupported database connection '".$connection_string."'", E_USER_ERROR);
}
}
/**
* Returns the SQL for the current database dialect which will return a two-column resultset containing a
* list of fields and their associated data types.
* @param string $tablename_string The name of the table we want fields from
*/
function GetFields( $tablename_string ) {
if ( !isset($this->dialect) ) {
trigger_error("Unsupported database dialect", E_USER_ERROR);
}
switch ( $this->dialect ) {
case 'pgsql':
$tablename_string = $this->Quote($tablename_string, 'identifier');
$sql = "SELECT f.attname, t.typname FROM pg_attribute f ";
$sql .= "JOIN pg_class c ON ( f.attrelid = c.oid ) ";
$sql .= "JOIN pg_type t ON ( f.atttypid = t.oid ) ";
$sql .= "WHERE relname = $tablename_string AND attnum >= 0 order by f.attnum;";
return $sql;
}
}
/**
* Translates the given SQL string into a form that will hopefully work for this database dialect. This hook
* is expected to be used by developers to provide support for differences in database operation by translating
* the query string in an arbitrary way, such as through a file or database lookup.
*
* The actual translation to other SQL dialects will usually be application-specific, so that any routines
* called by this will usually be external to this library, or will use resources external to this library.
*/
function Translate( $sql_string ) {
// Noop for the time being...
return $sql_string;
}
/**
* Returns $value escaped in an appropriate way for this database dialect.
* @param mixed $value The value to be escaped
* @param string $value_type The type of escaping desired. If blank this will be worked out from gettype($value). The special
* type of 'identifier' can also be used for escaping of SQL identifiers.
*/
function Quote( $value, $value_type = null ) {
if ( !isset($value_type) ) {
$value_type = gettype($value);
}
switch ( $value_type ) {
case 'identifier': // special case will only happen if it is passed in.
$rv = '"' . str_replace('"', '\\"', $value ) . '"';
break;
case 'null':
$rv = 'NULL';
break;
case 'integer':
case 'double' :
return $str;
case 'boolean':
$rv = $str ? 'TRUE' : 'FALSE';
break;
case 'string':
default:
$str = str_replace("'", "''", $str);
if ( strpos( $str, '\\' ) !== false ) {
$str = str_replace('\\', '\\\\', $str);
if ( $this->dialect == 'pgsql' ) {
/** PostgreSQL wants to know when a string might contain escapes */
$rv = "E'$str'";
}
}
}
return $rv;
}
/**
* Replaces query parameters with appropriately escaped substitutions.
*
* The function takes a variable number of arguments, the first is the
* SQL string, with replaceable '?' characters (a la DBI). The subsequent
* parameters being the values to replace into the SQL string.
*
* The values passed to the routine are analyzed for type, and quoted if
* they appear to need quoting. This can go wrong for (e.g.) NULL or
* other special SQL values which are not straightforwardly identifiable
* as needing quoting (or not). In such cases the parameter can be forced
* to be inserted unquoted by passing it as "array( 'plain' => $param )".
*
* @param string The query string with replacable '?' characters.
* @param mixed The values to replace into the SQL string.
* @return The built query string
*/
function ReplaceParameters() {
$argc = func_num_args();
$qry = func_get_arg(0);
$args = func_get_args();
if ( is_array($qry) ) {
/**
* If the first argument is an array we treat that as our arguments instead
*/
$qry = $args[0][0];
$args = $args[0];
$argc = count($args);
}
/**
* We only split into a maximum of $argc chunks. Any leftover ? will remain in
* the string and may be replaced at Exec rather than Prepare.
*/
$parts = explode( '?', $qry, $argc );
$querystring = $parts[0];
$z = count($parts);
for( $i = 1; $i < $z; $i++ ) {
$arg = $args[$i];
if ( !isset($arg) ) {
$querystring .= 'NULL';
}
elseif ( is_array($arg) && $arg['plain'] != '' ) {
// We abuse this, but people should access it through the PgQuery::Plain($v) function
$querystring .= $arg['plain'];
}
else {
$querystring .= $this->Quote($arg); //parameter
}
$querystring .= $parts[$i]; //extras eg. ","
}
if ( isset($parts[$z]) ) $querystring .= $parts[$z]; //puts last part on the end
return $querystring;
}
}
/**
* Typically there will only be a single instance of the database level class in an application.
* @package awl
*/
class PdoDatabase {
/**#@+
* @access private
*/
/**
* Holds the PDO database connection
*/
private $db;
/**
* Holds the dialect object
*/
private $dialect;
/**
* Holds the state of the transaction 0 = not started, 1 = in progress, -1 = error pending rollback/commit
*/
protected $txnstate = 0;
/**
* Holds the count of queries executed so far
*/
protected $querycount = 0;
/**
* Holds the total duration of queries executed so far
*/
protected $querytime = 0;
/**#@-*/
/**
* The connection string is in the standard PDO format. The database won't actually be connected until the first
* database query is run against it.
*
* The database object will also initialise and hold an PdoDialect object which will be used to provide database
* specific SQL for some queries, as well as translation hooks for instances where it is necessary to modify the
* SQL in transit to support additional databases.
* @param string $connection_string The PDO connection string, in all it's glory
* @param string $dbuser The database username to connect as
* @param string $dbpass The database password to connect with
* @param array $options An array of driver options
*/
function __construct( $connection_string, $dbuser=null, $dbpass=null, $options=null ) {
$this->dialect = new PdoDialect( $connection_string );
$this->db = new PDO( $connection_string, $dbuser, $dbpass, $options );
}
/**
* Returns a PdoQuery object created using this database, the supplied SQL string, and any parameters given.
* @param string $sql_query_string The SQL string containing optional variable replacements
* @param mixed ... Subsequent arguments are positionally replaced into the $sql_query_string
*/
function Prepare( ) {
$qry = new PdoQuery( $this );
$qry->Query(func_get_args());
return $qry;
}
/**
* Construct and execute an SQL statement from the sql_string, replacing the parameters into it.
*
* @param string $sql_query_string The SQL string containing optional variable replacements
* @param mixed ... Subsequent arguments are positionally replaced into the $sql_query_string
* @return mixed false on error or number of rows affected. Test failure with === false
*/
function Exec( ) {
$sql_string = $this->dialect->ReplaceParameters(func_get_args());
$start = microtime(true);
$result = $db->exec($sql_string);
$duration = microtime(true) - $start;
$this->querytime += $duration;
$this->querycount++;
return $result;
}
/**
* Begin a transaction.
*/
function Begin() {
if ( $this->txnstate == 0 ) {
$this->db->beginTransaction();
$this->txnstate = 1;
}
else {
trigger_error("Cannot begin a transaction while a transaction is already active.", E_USER_ERROR);
}
}
/**
* Complete a transaction.
*/
function Commit() {
$this->txnstate = 0;
if ( $this->txnstate != 0 ) {
$this->db->commit();
}
}
/**
* Cancel a transaction in progress.
*/
function Rollback() {
$this->txnstate = 0;
if ( $this->txnstate != 0 ) {
$this->db->rollBack();
}
else {
trigger_error("Cannot rollback unless a transaction is already active.", E_USER_ERROR);
}
}
/**
* Returns the current state of a transaction, indicating if we have begun a transaction, whether the transaction
* has failed, or if we are not in a transaction.
*/
function TransactionState() {
return $this->txnstate;
}
/**
* Returns the total duration of quries executed so far by this object instance.
*/
function TotalDuration() {
return $this->querytime;
}
/**
* Returns the total number of quries executed by this object instance.
*/
function TotalQueries() {
return $this->querycount;
}
/**
* Returns an associative array of field types, keyed by field name, for the requested named table. Internally this
* calls PdoDialect::GetFields to get the required SQL and then processes the query in the normal manner.
*/
function GetFields( $tablename_string ) {
}
/**
* Operates identically to PdoDatabase::Prepare, except that PdoDialect::Translate() will be called on the query
* before any processing.
*/
function PrepareTranslated() {
}
/**
* Switches on or off the processing flag controlling whether subsequent calls to PdoDatabase::Prepare are translated
* as if PrepareTranslated() had been called.
*/
function TranslateAll( $onoff_boolean ) {
}
}
/**
* A variable of this class is normally constructed through a call to PdoDatabase::Query or PdoDatabase::Prepare,
* associating it on construction with the database which is to be queried.
* @package awl
*/
class PdoQuery {
private $pdb;
private $sth;
private $max_duration = 2;
/**
* Where $db is a PdoDatabase object. This constructs the PdoQuery. If there are further parameters they
* will be in turn, the sql, and any positional parameters to replace into that, and will be passed to
* $this->Query() before returning.
*/
function __construct( ) {
$args = func_get_args();
$this->pdb = array_shift( $args );
if ( isset($db->default_max_duration) ) {
$this->max_duration = $db->default_max_duration;
}
$this->Query($args);
}
/**
* If the sql is supplied then PDO::prepare will be called with that SQL to prepare the query, and if there
* are positional parameters then they will be replaced into the sql_string (with appropriate escaping)
* before the call to PDO::prepare. Query preparation time is counted towards total query execution time.
*/
function Query( ) {
$sql_string = $this->dialect->ReplaceParameters(func_get_args());
$start = microtime(true);
$this->sth = $pdb->db->prepare($sql_string);
$duration = microtime(true) - $start;
$this->querytime += $duration;
}
/**
* If there are (some) positional parameters in the prepared query, now is the last chance to supply them...
* before the query is executed. Returns true on success and false on error.
*/
function Exec( ) {
$start = microtime(true);
$result = $this->sth->execute(func_get_args());
$duration = microtime(true) - $start;
$this->querytime += $duration;
$this->querycount++;
return $result;
}
/**
* Will fetch the next row from the query into an object with elements named for the fields in the result.
*/
function Fetch() {
return $this->sth->fetchObject();
}
/**
* Will fetch the next row from the query into an array with numbered elements and with elements named
* for the fields in the result.
*/
function FetchArray() {
return $this->sth->fetch();
}
/**
* Will fetch all result rows from the query into an array of objects with elements named for the fields in the result.
*/
function FetchAll() {
return $this->sth->fetchAll(PDO::FETCH_OBJ);
}
/**
* An accessor for the number of rows affected when the query was executed.
*/
function Rows() {
return $this->sth->rowCount();
}
/**
* Used to set the maximum duration for this query before it will be logged as a slow query.
* @param double $seconds The maximum duration for this statement before logging it as 'slow'
*/
function MaxDuration( $seconds ) {
$this->max_duration = $seconds;
}
}