From 6fee9fcb024f689542c0b784b76d8631d734b0f6 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Mon, 26 Oct 2009 00:09:31 +1300 Subject: [PATCH] What the plans for PdoQuery have turned into... --- inc/AwlDBDialect.php | 293 ++++++++++++++++++++++++++ inc/AwlDatabase.php | 152 ++++++++++++++ inc/AwlQuery.php | 438 ++++++++++++++++++++++++++++++++++++++ inc/PdoQuery.php | 488 ------------------------------------------- 4 files changed, 883 insertions(+), 488 deletions(-) create mode 100644 inc/AwlDBDialect.php create mode 100644 inc/AwlDatabase.php create mode 100644 inc/AwlQuery.php delete mode 100644 inc/PdoQuery.php diff --git a/inc/AwlDBDialect.php b/inc/AwlDBDialect.php new file mode 100644 index 00000000..0c4ca89d --- /dev/null +++ b/inc/AwlDBDialect.php @@ -0,0 +1,293 @@ + +* @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; + } + + + +} diff --git a/inc/AwlDatabase.php b/inc/AwlDatabase.php new file mode 100644 index 00000000..6f324356 --- /dev/null +++ b/inc/AwlDatabase.php @@ -0,0 +1,152 @@ +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 +* @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 ) { + } + +} + + diff --git a/inc/AwlQuery.php b/inc/AwlQuery.php new file mode 100644 index 00000000..70c078ec --- /dev/null +++ b/inc/AwlQuery.php @@ -0,0 +1,438 @@ + +* @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 <<Database Connection Failure +

Database Error

+

Could not connect to database

+ + +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. +* +* Example usage +* +* $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); +* } +* } +* +* +* @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. + * + * Example + * So that you can nicely enable/disable the queries for a particular class, you + * could use some of PHPs magic constants in your call. + * + * $qry->Exec(__CLASS__, __LINE__, __FILE__); + * + * + * + * @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; + } + + +} + diff --git a/inc/PdoQuery.php b/inc/PdoQuery.php deleted file mode 100644 index b8ac6c63..00000000 --- a/inc/PdoQuery.php +++ /dev/null @@ -1,488 +0,0 @@ -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 -* @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; - } - -} -