From fe1b1369ff9cc44ffb69cb28fb4b10ed7c0982cd Mon Sep 17 00:00:00 2001 From: Matt Pelmear Date: Fri, 11 Mar 2011 00:11:19 -0500 Subject: [PATCH 1/7] Initial commit of DB class progress --- system/class/DB.class.php | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 system/class/DB.class.php diff --git a/system/class/DB.class.php b/system/class/DB.class.php new file mode 100644 index 0000000..cae5590 --- /dev/null +++ b/system/class/DB.class.php @@ -0,0 +1,121 @@ +setTZ( DB::$tz ); + break; + + case DB::MODE_READONLY: + if( !isset(DB::$connections[$name]) ) + DB::$connections[$name] = array(); + if( !isset(DB::$connections[$name][DB::MODE_READONLY]) ) + DB::$connections[$name][DB::MODE_READONLY] = array( 'con' => array(), 'last_used' => -1 ); + $id = count(DB::$connections[$name][DB::MODE_READONLY]['con']); + + $con = new DBConnector( $dsn, $user, $pass ); + $con->setTZ( DB::$tz ); + DB::$connections[$name][DB::MODE_READONLY]['con'][$id] = $con; + + break; + + default: + throw new Exception( str_replace( '%mode%', $mode, DB::ERR_INVALID_DBCONN_MODE ) ); + } + + if( count(DB::$connections) == 1 ) + DB::setDefault( $name ); + } + + static public function removeConnection( $name, $mode ) + { + + } + + static public function setDefault( $name ) + { + + } + } + From f49678306542cf4a5b22a44fe5508fd0909bb2bf Mon Sep 17 00:00:00 2001 From: Matt Pelmear Date: Fri, 11 Mar 2011 00:27:24 -0500 Subject: [PATCH 2/7] Some work on DB.class --- system/class/DB.class.php | 120 ++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/system/class/DB.class.php b/system/class/DB.class.php index cae5590..5b03189 100644 --- a/system/class/DB.class.php +++ b/system/class/DB.class.php @@ -1,5 +1,12 @@ + */ class DB { const MODE_AUTO = 'AUTO'; @@ -8,54 +15,88 @@ class DB const ERR_NAME_EMPTY_STRING = 'DB Connector Name cannot be an empty string'; const ERR_INVALID_DBCONN_MODE = 'Invalid DB connection mode: "%mode%"'; + const ERR_NO_SUCH_CONNECTION = 'No such DB connection: "%name%" (%mode%)'; static private $connections = array(); static private $default_conn = NULL; static private $tz = NULL; - static public function getCol( $sql, $col=0, $db=NULL ) + static public function getCol( $sql, $db=NULL ) { + return DB::doDBcall( 'getCol', $sql, $db ); + } + static public function getColN( $sql, $col, $db=NULL ) + { +//TODO: getColN() } static public function getAll( $sql, $db=NULL ) { - + return DB::doDBcall( 'getAll', $sql, $db ); } static public function getRow( $sql, $db=NULL ) { - + return DB::doDBcall( 'getRow', $sql, $db ); } static public function getAssoc( $sql, $db=NULL ) { - + return DB::doDBcall( 'getAssoc', $sql, $db ); } static public function getOne( $sql, $db=NULL ) { - + return DB::doDBcall( 'getOne', $sql, $db ); } static public function query( $sql, $db=NULL ) { - + return DB::doDBcall( 'query', $sql, $db ); } static public function lowPriorityQuery( $sql, $db=NULL ) { - +//TODO: DB::lowPriorityQuery() } - static public function getLastInsertID( $db=NULL ) + /** + * Returns the insert ID from the last query that was run. + * @params String $db + * @return Int + */ + static public function getLastInsertID( $db='default' ) { - + if( !isset(DB::$connections[$db][DB::MODE_READWRITE]) ) + throw new Exception( str_replace( '%name%', $db, + str_replace( '%mode%', $mode, + DB::ERR_NO_SUCH_CONNECTION ))); + + return DB::$connections[$db][DB::MODE_READWRITE]->getLastInsertID(); + } + + static public function doDBcall( $method, $sql, $db ) + { +//TODO: DB::doDBcall() } + /** + * @param String $db (Optional; Defaults to NULL, meaning the default connector) + * @param String $mode (See DB::MODE_READWRITE, DB::MODE_READONLY) + * @return Bool + */ static public function isConnected( $db=NULL, $mode=DB::MODE_READWRITE ) { - + if( $db === NULL ) + $db = 'default'; + + if( !isset(DB::$connections[$name][$mode]) ) + throw new Exception( str_replace( '%name%', $db, + str_replace( '%mode%', $mode, + DB::ERR_NO_SUCH_CONNECTION ))); + + return DB::$connections[$name][$mode]->isConnected(); } /** @@ -108,14 +149,69 @@ static public function addConnection( $name, $mode, $dsn, $user=NULL, $pass=NULL DB::setDefault( $name ); } + /** + * Removes a connector from the pool of available connectors. + * If you remove the READONLY connection, this will remove all READONLY connectors. + * @param String $name + * @param String $mode (See DB::MODE_READWRITE, DB::MODE_READONLY) + */ static public function removeConnection( $name, $mode ) { - +//TODO: What to do when we remove the default connector? + $name = (String) $name; + switch( $mode ) + { + case DB::MODE_READWRITE: + if( isset(DB::$connections[$name][DB::MODE_READWRITE]) ) + DB::$connections[$name][DB::MODE_READWRITE]->disconnect(); + else + throw new Exception( str_replace( '%name%', $name, + str_replace( '%mode%', DB::MODE_READWRITE, + DB::ERR_NO_SUCH_CONNECTION ))); + break; + + case DB::MODE_READONLY: + if( isset(DB::$connections[$name][DB::MODE_READONLY]) ) + { + foreach( DB::$connections[$name][DB::MODE_READONLY]['con'] as $db ) + $db->disconnect(); + } + else + throw new Exception( str_replace( '%name%', $name, + str_replace( '%mode%', DB::MODE_READONLY, + DB::ERR_NO_SUCH_CONNECTION ))); + break; + + default: + throw new Exception( str_replace( '%mode%', $mode, DB::ERR_INVALID_DBCONN_MODE ) ); + } } - static public function setDefault( $name ) + /** + * Disconnects all connectors + */ + static public function disconnectAll() { + foreach( DB::$connections as $conn ) + { + if( isset($conn[DB::MODE_READWRITE]) ) + $conn[DB::MODE_READWRITE]->disconnect(); + if( isset($conn[DB::MODE_READONLY]) ) + { + foreach( $conn[DB::MODE_READONLY]['con'] as $ro ) + $ro->disconnect(); + } + } + } + /** + * Sets connector with name $name as the default to be used. + * @param String $name + */ + static public function setDefault( $name ) + { + DB::$default_conn = $name; + DB::$connections['default'] =& DB::$connections[$name]; } } From 0468150add6cb4f72d64e01780dd1f8a7b1cde90 Mon Sep 17 00:00:00 2001 From: Matt Pelmear Date: Mon, 14 Mar 2011 00:20:14 -0400 Subject: [PATCH 3/7] Adding DBConnector class --- system/class/DBConnector.class.php | 299 +++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 system/class/DBConnector.class.php diff --git a/system/class/DBConnector.class.php b/system/class/DBConnector.class.php new file mode 100644 index 0000000..29da1cf --- /dev/null +++ b/system/class/DBConnector.class.php @@ -0,0 +1,299 @@ +connected = FALSE; + $this->tz = DB::DEFAULT_TZ; + $this->dsn = $dsn; + $this->user = $user; + $this->pass = $pass; + } + + private function con() + { + if( !$this->connected ) + $this->connect(); + } + + public function connect() + { + if( $this->connected ) return TRUE; + + try { + // Without this check, PDO seems to lose 216 bytes or so + // of memory on failure of connect... -mpelmear + if( $this->dsn == '' ) + throw new Exception( DBConnector::ERR_CANNOT_CONNECT ); + $this->pdo = new PDO( $this->dsn, $this->user, $this->pass ); + } catch( PDOException $pdo ) { + throw new Exception( DBConnector::ERR_CANNOT_CONNECT ); + } + + $this->pdo->exec( 'SET time_zone="' . $this->tz . '"' ); + $this->connected = TRUE; + + return TRUE; + } + + public function disconnect() + { + $this->pdo = NULL; + $this->connected = FALSE; + } + + /** + * Sets connection TimeZone. + * @param String $tz MySQL-compatible timezone + */ + public function setTZ( $tz ) + { + $this->tz = $tz; + if( $this->connected ) + $this->pdo->exec( 'SET time_zone="' . $this->tz . '"' ); + } + + /** + * @param SQL $sql + * @param uInt $col (optional, defaults to 0) + * @return Array + * @throws PDOException + */ + public function getCol( $sql, $col = 0 ) + { + $this->con(); + $q = $this->_query( $sql ); + + $ret = $q->fetchAll(PDO::FETCH_COLUMN, $col); + if ($sql->willCalcFoundRows() && $q->nextRowset()) + $sql->setFoundRowsCallback($q->fetchColumn()); + return $ret; + } + + /** + * @param SQL + * @return Array + * @throws PDOException + */ + public function getAll( $sql ) + { + $this->con(); + $s = $this->_query( $sql ); + + $ret = $s->fetchAll(PDO::FETCH_ASSOC); + if ($sql->willCalcFoundRows() && $s->nextRowset()) + $sql->setFoundRowsCallback($s->fetchColumn()); + return $ret; + } + + /** + * @param SQL + * @return Array + * @throws PDOException + */ + public function getRow( $sql ) + { + $this->con(); + $q = $this->_query( $sql ); + + $ret = $q->fetch(PDO::FETCH_ASSOC); + if ($sql->willCalcFoundRows() && $q->nextRowset()) + $sql->setFoundRowsCallback($q->fetchColumn()); + return $ret; + } + + /** + * If there are two columns in the resultset, return a k=>v array + * with col1 as key, col2 as value. + * If there are more than two columns in the resultset, + * return a k=>v array where col1 is the key and the + * remainder of the columns stay in an array as the value. + * @param SQL + * @return Array + * @throws PDOException + */ + public function getAssoc( $sql ) + { + $this->con(); + $sth = $this->_query( $sql ); + + if( !is_object( $sth ) ) + $this->throwError(); + + $rs = array(); + + if( $sth->columnCount() == 2 ) + { + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) + $rs[array_shift($row)] = array_shift($row); + } + else + { + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) + $rs[array_shift($row)] = $row; + } + if ($sql->willCalcFoundRows() && $sth->nextRowset()) + $sql->setFoundRowsCallback($sth->fetchColumn()); + return $rs; + } + + /** + * Returns the first column from the first row of the result set. + * @param SQL + * @return Mixed + * @throws PDOException + */ + public function getOne( $sql ) + { + $this->con(); + $sth = $this->_query( $sql ); + + $ret = $sth ? $sth->fetchColumn() : NULL; + if ($sql->willCalcFoundRows() && $sth->nextRowset()) + $sql->setFoundRowsCallback($sth->fetchColumn()); + return $ret; + } + + /** + * Use for buffered queries and non-SELECT statements. + * Returns DBRecordSet for SELECT statements, + * Returns uInt (number of rows affected) for other statements. + * @param SQL + * @return DBRecordSet|uInt + * @throws PDOException + */ + public function query( $sql ) + { + $this->con(); + + $q = $sql->getSQL(); + if( strtoupper(substr(ltrim($q), 0, 6)) == 'SELECT' ) + { + $res = $this->_query( $sql ); + return new DBRecordSet( $res ); + } + else + return $this->_exec( $sql ); + } + + /** + * Use only for statements that only return number of rows affected. + * (INSERT, UPDATE, etc.) + * @param SQL + * @return uInt (Number of rows affected) + * @throws PDOException + */ + public function exec( $sql ) + { + $this->con(); + return $this->_exec( $sql ); + } + + /** + * Internally execute various types of query calls. + * @param SQL $sql + * @param String $query_or_exec (either 'exec' or 'query') + */ + private function _query( $sql, $query_or_exec='query' ) + { + $bind_style_params = $this->prepareBindStyleParams(); + + if( $query_or_exec == 'exec' ) + $r = $this->pdo->exec( $sql->getSQL( $bind_style_params ) ); + else + $r = $this->pdo->query( $sql->getSQL( $bind_style_params ) ); + + if( $r === FALSE ) + { + // There was an error. See what to do + $error_info = $this->pdo->errorInfo(); + if( $error_info[0] == 'HY000' && $error_info[1] == '2006' ) + { + // HY000/2006 seems to be the error we get when we lost our connection. + // Try to reconnect and re-run the query. + $this->disconnect(); + $this->connect(); + + if( $query_or_exec == 'exec' ) + $r = $this->pdo->exec( $sql->getSQL( $bind_style_params ) ); + else + $r = $this->pdo->query( $sql->getSQL( $bind_style_params ) ); + + if( $r === FALSE ) + $this->throwError(); + } + else + $this->throwError(); + } + + return $r; + } + + private function _exec( $sql ) + { + return $this->_query( $sql, 'exec' ); + } + + private function prepareBindStyleParams() + { + $bind_style_params = array(); + switch( $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) ) + { + case 'mysql': + $bind_style_params = array( SQL::PARAM_ESCAPE_STYLE => SQL::ESCAPE_STYLE_MYSQL ); + break; + case 'sqlite': + $bind_style_params = array( SQL::PARAM_ESCAPE_STYLE => SQL::ESCAPE_STYLE_SQLITE ); + break; + default: + throw new Exception( 'Unsupported database driver: ' . $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) ); + } + + return $bind_style_params; + } + + /** + * Verifies that $sql is a valid SQL object. + * @param Mixed $sql + * @throws Exception( DBConnector::ERR_EXPECTED_SQL_OBJ ) + */ + public static function checkSQLobject( $sql ) + { + if( !is_object( $sql ) ) + throw new Exception( DBConnector::ERR_EXPECTED_SQL_OBJ ); + if( get_class( $sql ) != 'SQL' ) + throw new Exception( DBConnector::ERR_EXPECTED_SQL_OBJ ); + } + + /** + * Gathers PDO exception data an throws an exception + * @throws PDOException + */ + private function throwError() + { +//TODO: Probably don't need to htmlspecialchars here... maybe that should happen on the UI layer? + $error = $this->pdo->errorInfo(); + throw new PDOException($error[0].' ('.$error[1].'): '.htmlspecialchars($error[2])); + } + + /** + * @return String Last insert ID for the current DB + */ + public function getLastInsertID() + { + return $this->pdo->lastInsertId(); + } + } + From 732d46cd200800bec343ba4e395ab93a0fc64a54 Mon Sep 17 00:00:00 2001 From: Matt Pelmear Date: Wed, 16 Mar 2011 00:00:34 -0400 Subject: [PATCH 4/7] Progress commit --- system/class/DB.class.php | 104 ++++++++++++++++++++++++- system/class/DBConnector.class.php | 117 +++++++++++++++++++++-------- system/class/SQL.class.php | 15 ++++ 3 files changed, 203 insertions(+), 33 deletions(-) create mode 100644 system/class/SQL.class.php diff --git a/system/class/DB.class.php b/system/class/DB.class.php index 5b03189..83bbab1 100644 --- a/system/class/DB.class.php +++ b/system/class/DB.class.php @@ -21,44 +21,86 @@ class DB static private $default_conn = NULL; static private $tz = NULL; + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Array|NULL + */ static public function getCol( $sql, $db=NULL ) { return DB::doDBcall( 'getCol', $sql, $db ); } - + + /** + * @param SQL $sql + * @param uInt $col + * @param String|NULL $db (Optional; defaults to NULL) + * @return Array|NULL + */ static public function getColN( $sql, $col, $db=NULL ) { //TODO: getColN() } + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Array|NULL + */ static public function getAll( $sql, $db=NULL ) { return DB::doDBcall( 'getAll', $sql, $db ); } + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Array|NULL + */ static public function getRow( $sql, $db=NULL ) { return DB::doDBcall( 'getRow', $sql, $db ); } + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Array|NULL + */ static public function getAssoc( $sql, $db=NULL ) { return DB::doDBcall( 'getAssoc', $sql, $db ); } + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Mixed + */ static public function getOne( $sql, $db=NULL ) { return DB::doDBcall( 'getOne', $sql, $db ); } + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Mixed + */ static public function query( $sql, $db=NULL ) { return DB::doDBcall( 'query', $sql, $db ); } + /** + * @param SQL $sql + * @param String|NULL $db (Optional; defaults to NULL) + * @return Mixed + */ static public function lowPriorityQuery( $sql, $db=NULL ) { //TODO: DB::lowPriorityQuery() + return DB::doDBcall( 'query', $sql, $db ); } /** @@ -76,9 +118,65 @@ static public function getLastInsertID( $db='default' ) return DB::$connections[$db][DB::MODE_READWRITE]->getLastInsertID(); } - static public function doDBcall( $method, $sql, $db ) + /** + * @param String $method + * @param SQL $sql + * @param String|NULL $db + * @return Mixed + */ + static private function doDBcall( $method, $sql, $db ) { -//TODO: DB::doDBcall() + if( $db === NULL ) + { + if( $sql->getDatabase() === NULL ) $db = 'default'; + else $db = $sql->getDatabase(); + } + + $sql->setDatabase( DB::$connections[$db]['name'] ); + $sql->setMode( $mode ); + + if( Debug::isEnabled() ) + { + $qid = Debug::registerQuery( $sql, $db ); + $qt_start = microtime(TRUE); + } + + switch( $mode ) + { + case DB::MODE_AUTO: +//TODO: Implement MODE_AUTO choosing instead of short circuiting to R/W connector +//TODO: Report for debugging which connector was actually chosen when we're doing AUTO + case DB::MODE_READWRITE: + if( !isset(DB::$connections[$db][DB::MODE_READWRITE]) ) + throw new Exception( + str_replace( '%name%', $db, + str_replace( '%mode%', $mode, DB::ERR_NO_SUCH_CONNECTION ))); + + $conn = DB::$connections[$db][DB::MODE_READWRITE]; + break; + case DB::MODE_READONLY: + $conn_number = DB::pickRO($db); + if( !isset(DB::$connections[$db][DB::MODE_READONLY]['con'][$conn_number]) ) + throw new Exception( + str_replace( '%name%', $db, + str_replace( '%mode%', $mode, DB::ERR_NO_SUCH_CONNECTION ))); + + $conn = DB::$connections[$db][DB::MODE_READONLY]['con'][$conn_number]; + break; + default: + throw new Exception( "unsupported mode" ); + } + + $result = $conn->$method($sql); + + if( Debug::isEnabled() ) + { + $qt_end = microtime(TRUE); + $query_time = round( $qt_end - $qt_start, 5 ); + Debug::registerQueryTime( $qid, $query_time ); + } + + return $result; } /** diff --git a/system/class/DBConnector.class.php b/system/class/DBConnector.class.php index 29da1cf..b51afd8 100644 --- a/system/class/DBConnector.class.php +++ b/system/class/DBConnector.class.php @@ -1,18 +1,34 @@ + */ class DBConnector { const ERR_CANNOT_CONNECT = 'Cannot connect to DB'; const ERR_EXPECTED_SQL_OBJ = 'Expected object of type SQL'; + const ERR_UNSUPPORTED_DRIVER = 'Unsupported database driver: "%driver%"'; + const QE_QUERY = 'query'; + const QE_EXEC = 'exec'; - public $pdo; + private $pdo; private $tz; private $dsn; private $user; private $pass; private $connected = FALSE; + /** + * @param String $dsn + * @param String $user (Optional; defaults to empty string) + * @param String $pass (Optional; defaults to empty string) + */ public function __construct( $dsn, $user = '', $pass = '' ) { $this->connected = FALSE; @@ -22,19 +38,24 @@ public function __construct( $dsn, $user = '', $pass = '' ) $this->pass = $pass; } + /** + * Internally connects to db server when necessary + */ private function con() { - if( !$this->connected ) - $this->connect(); + if( !$this->connected ) $this->connect(); } + /** + * Publicly accessible method to force connection to db server + */ public function connect() { if( $this->connected ) return TRUE; try { // Without this check, PDO seems to lose 216 bytes or so - // of memory on failure of connect... -mpelmear + // of memory on failure to connect to an empty string connector... -mpelmear if( $this->dsn == '' ) throw new Exception( DBConnector::ERR_CANNOT_CONNECT ); $this->pdo = new PDO( $this->dsn, $this->user, $this->pass ); @@ -48,6 +69,9 @@ public function connect() return TRUE; } + /** + * Publicly accessible method to force disconnection from db server + */ public function disconnect() { $this->pdo = NULL; @@ -65,20 +89,40 @@ public function setTZ( $tz ) $this->pdo->exec( 'SET time_zone="' . $this->tz . '"' ); } + /** + * @param SQL $sql + * @return Array + * @throws PDOException + */ + public function getCol( $sql ) + { + self::checkSQLobject( $sql ); + $this->con(); + $q = $this->_query( $sql ); + + $ret = $q->fetchAll(PDO::FETCH_COLUMN, 0); + if( $sql->willCalcFoundRows() && $q->nextRowset() ) + $sql->setFoundRowsCallback( $q->fetchColumn() ); + + return $ret; + } + /** * @param SQL $sql * @param uInt $col (optional, defaults to 0) * @return Array * @throws PDOException */ - public function getCol( $sql, $col = 0 ) + public function getColN( $sql, $col=0 ) { + self::checkSQLobject( $sql ); $this->con(); $q = $this->_query( $sql ); $ret = $q->fetchAll(PDO::FETCH_COLUMN, $col); - if ($sql->willCalcFoundRows() && $q->nextRowset()) - $sql->setFoundRowsCallback($q->fetchColumn()); + if( $sql->willCalcFoundRows() && $q->nextRowset() ) + $sql->setFoundRowsCallback( $q->fetchColumn() ); + return $ret; } @@ -89,12 +133,14 @@ public function getCol( $sql, $col = 0 ) */ public function getAll( $sql ) { + self::checkSQLobject( $sql ); $this->con(); $s = $this->_query( $sql ); $ret = $s->fetchAll(PDO::FETCH_ASSOC); - if ($sql->willCalcFoundRows() && $s->nextRowset()) - $sql->setFoundRowsCallback($s->fetchColumn()); + if( $sql->willCalcFoundRows() && $s->nextRowset() ) + $sql->setFoundRowsCallback( $s->fetchColumn() ); + return $ret; } @@ -105,12 +151,14 @@ public function getAll( $sql ) */ public function getRow( $sql ) { + self::checkSQLobject( $sql ); $this->con(); $q = $this->_query( $sql ); $ret = $q->fetch(PDO::FETCH_ASSOC); - if ($sql->willCalcFoundRows() && $q->nextRowset()) - $sql->setFoundRowsCallback($q->fetchColumn()); + if( $sql->willCalcFoundRows() && $q->nextRowset() ) + $sql->setFoundRowsCallback( $q->fetchColumn() ); + return $ret; } @@ -126,6 +174,7 @@ public function getRow( $sql ) */ public function getAssoc( $sql ) { + self::checkSQLobject( $sql ); $this->con(); $sth = $this->_query( $sql ); @@ -136,16 +185,17 @@ public function getAssoc( $sql ) if( $sth->columnCount() == 2 ) { - while ($row = $sth->fetch(PDO::FETCH_ASSOC)) + while( $row = $sth->fetch(PDO::FETCH_ASSOC) ) $rs[array_shift($row)] = array_shift($row); } else { - while ($row = $sth->fetch(PDO::FETCH_ASSOC)) + while( $row = $sth->fetch(PDO::FETCH_ASSOC) ) $rs[array_shift($row)] = $row; } - if ($sql->willCalcFoundRows() && $sth->nextRowset()) - $sql->setFoundRowsCallback($sth->fetchColumn()); + if( $sql->willCalcFoundRows() && $sth->nextRowset() ) + $sql->setFoundRowsCallback( $sth->fetchColumn() ); + return $rs; } @@ -157,12 +207,14 @@ public function getAssoc( $sql ) */ public function getOne( $sql ) { + self::checkSQLobject( $sql ); $this->con(); $sth = $this->_query( $sql ); $ret = $sth ? $sth->fetchColumn() : NULL; - if ($sql->willCalcFoundRows() && $sth->nextRowset()) - $sql->setFoundRowsCallback($sth->fetchColumn()); + if( $sql->willCalcFoundRows() && $sth->nextRowset() ) + $sql->setFoundRowsCallback( $sth->fetchColumn() ); + return $ret; } @@ -176,6 +228,7 @@ public function getOne( $sql ) */ public function query( $sql ) { + self::checkSQLobject( $sql ); $this->con(); $q = $sql->getSQL(); @@ -197,6 +250,7 @@ public function query( $sql ) */ public function exec( $sql ) { + self::checkSQLobject( $sql ); $this->con(); return $this->_exec( $sql ); } @@ -204,13 +258,13 @@ public function exec( $sql ) /** * Internally execute various types of query calls. * @param SQL $sql - * @param String $query_or_exec (either 'exec' or 'query') + * @param String $query_or_exec (Optional; Defaults to QE_QUERY. Either DBConnector::QE_EXEC or DBConnector::QE_QUERY) */ - private function _query( $sql, $query_or_exec='query' ) + private function _query( $sql, $query_or_exec = self::QE_QUERY ) { $bind_style_params = $this->prepareBindStyleParams(); - if( $query_or_exec == 'exec' ) + if( $query_or_exec == DBConnector::QE_EXEC ) $r = $this->pdo->exec( $sql->getSQL( $bind_style_params ) ); else $r = $this->pdo->query( $sql->getSQL( $bind_style_params ) ); @@ -226,7 +280,7 @@ private function _query( $sql, $query_or_exec='query' ) $this->disconnect(); $this->connect(); - if( $query_or_exec == 'exec' ) + if( $query_or_exec == DBConnector::QE_EXEC ) $r = $this->pdo->exec( $sql->getSQL( $bind_style_params ) ); else $r = $this->pdo->query( $sql->getSQL( $bind_style_params ) ); @@ -243,25 +297,29 @@ private function _query( $sql, $query_or_exec='query' ) private function _exec( $sql ) { - return $this->_query( $sql, 'exec' ); + return $this->_query( $sql, DBConnector::QE_EXEC ); } + /** + * Returns parameters suitable to be passed to SQL class + * so that binding is appropriate for the connector in use. + * @return Array + */ private function prepareBindStyleParams() { $bind_style_params = array(); - switch( $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) ) + $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + switch( $driver ) { case 'mysql': - $bind_style_params = array( SQL::PARAM_ESCAPE_STYLE => SQL::ESCAPE_STYLE_MYSQL ); + return array( SQL::PARAM_ESCAPE_STYLE => SQL::ESCAPE_STYLE_MYSQL ); break; case 'sqlite': - $bind_style_params = array( SQL::PARAM_ESCAPE_STYLE => SQL::ESCAPE_STYLE_SQLITE ); + return array( SQL::PARAM_ESCAPE_STYLE => SQL::ESCAPE_STYLE_SQLITE ); break; default: - throw new Exception( 'Unsupported database driver: ' . $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) ); + throw new Exception( str_replace( '%driver%', $driver, DBConnector::ERR_UNSUPPORTED_DRIVER ) ); } - - return $bind_style_params; } /** @@ -283,9 +341,8 @@ public static function checkSQLobject( $sql ) */ private function throwError() { -//TODO: Probably don't need to htmlspecialchars here... maybe that should happen on the UI layer? $error = $this->pdo->errorInfo(); - throw new PDOException($error[0].' ('.$error[1].'): '.htmlspecialchars($error[2])); + throw new PDOException( $error[0].' ('.$error[1].'): '.$error[2] ); } /** diff --git a/system/class/SQL.class.php b/system/class/SQL.class.php new file mode 100644 index 0000000..4c540ed --- /dev/null +++ b/system/class/SQL.class.php @@ -0,0 +1,15 @@ + + */ + class SQL + { + + } + From a82311249d033bd3b567e4079925ad5fbfab6662 Mon Sep 17 00:00:00 2001 From: Matt Pelmear Date: Tue, 22 Mar 2011 23:18:38 -0400 Subject: [PATCH 5/7] Working DB class, for basic things anyway --- system/class/DB.class.php | 5 +++- system/class/DBConnector.class.php | 2 +- system/class/SQL.class.php | 46 +++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/system/class/DB.class.php b/system/class/DB.class.php index 83bbab1..58354e0 100644 --- a/system/class/DB.class.php +++ b/system/class/DB.class.php @@ -16,6 +16,8 @@ class DB const ERR_NAME_EMPTY_STRING = 'DB Connector Name cannot be an empty string'; const ERR_INVALID_DBCONN_MODE = 'Invalid DB connection mode: "%mode%"'; const ERR_NO_SUCH_CONNECTION = 'No such DB connection: "%name%" (%mode%)'; + + const DEFAULT_TZ = 'US/Eastern'; static private $connections = array(); static private $default_conn = NULL; @@ -126,13 +128,14 @@ static public function getLastInsertID( $db='default' ) */ static private function doDBcall( $method, $sql, $db ) { +$mode = DB::MODE_AUTO; if( $db === NULL ) { if( $sql->getDatabase() === NULL ) $db = 'default'; else $db = $sql->getDatabase(); } - $sql->setDatabase( DB::$connections[$db]['name'] ); + $sql->setDatabase( DB::$connections[$db] ); $sql->setMode( $mode ); if( Debug::isEnabled() ) diff --git a/system/class/DBConnector.class.php b/system/class/DBConnector.class.php index b51afd8..af41b86 100644 --- a/system/class/DBConnector.class.php +++ b/system/class/DBConnector.class.php @@ -60,7 +60,7 @@ public function connect() throw new Exception( DBConnector::ERR_CANNOT_CONNECT ); $this->pdo = new PDO( $this->dsn, $this->user, $this->pass ); } catch( PDOException $pdo ) { - throw new Exception( DBConnector::ERR_CANNOT_CONNECT ); + throw new Exception( DBConnector::ERR_CANNOT_CONNECT, $pdo ); } $this->pdo->exec( 'SET time_zone="' . $this->tz . '"' ); diff --git a/system/class/SQL.class.php b/system/class/SQL.class.php index 4c540ed..f372423 100644 --- a/system/class/SQL.class.php +++ b/system/class/SQL.class.php @@ -10,6 +10,50 @@ */ class SQL { - + const PARAM_ESCAPE_STYLE = 'param-escape-style'; + const ESCAPE_STYLE_MYSQL = 'mysql'; + const ESCAPE_STYLE_SQLITE = 'sqlite'; + + private $sql; + + function __construct( $sql ) + { +//TODO + $this->sql = $sql; + } + + static public function bind( $sql ) + { +//TODO + $q = new SQL( $sql ); + return $q; + } + + public function setDatabase( $db ) + { +//TODO + } + + public function getDatabase() + { + return NULL; + } + + public function setMode( $mode ) + { +//TODO + } + + public function willCalcFoundRows() + { +//TODO + return FALSE; + } + + public function getSQL() + { +//TODO + return $this->sql; + } } From f41a8562cbe9d6d6e834949d1d864b3020a00c9d Mon Sep 17 00:00:00 2001 From: Matt Pelmear Date: Wed, 28 Mar 2012 23:59:48 -0700 Subject: [PATCH 6/7] DB connector and SQL class --- system/class/DBConnector.class.php | 6 +- system/class/Debug.class.php | 9 + system/class/SQL.class.php | 419 +++++++++++++++++++++++++++-- 3 files changed, 411 insertions(+), 23 deletions(-) create mode 100644 system/class/Debug.class.php diff --git a/system/class/DBConnector.class.php b/system/class/DBConnector.class.php index af41b86..c9d5df9 100644 --- a/system/class/DBConnector.class.php +++ b/system/class/DBConnector.class.php @@ -60,7 +60,7 @@ public function connect() throw new Exception( DBConnector::ERR_CANNOT_CONNECT ); $this->pdo = new PDO( $this->dsn, $this->user, $this->pass ); } catch( PDOException $pdo ) { - throw new Exception( DBConnector::ERR_CANNOT_CONNECT, $pdo ); + throw new Exception( DBConnector::ERR_CANNOT_CONNECT, NULL, $pdo ); } $this->pdo->exec( 'SET time_zone="' . $this->tz . '"' ); @@ -329,9 +329,7 @@ private function prepareBindStyleParams() */ public static function checkSQLobject( $sql ) { - if( !is_object( $sql ) ) - throw new Exception( DBConnector::ERR_EXPECTED_SQL_OBJ ); - if( get_class( $sql ) != 'SQL' ) + if( !($sql instanceof SQL) ) throw new Exception( DBConnector::ERR_EXPECTED_SQL_OBJ ); } diff --git a/system/class/Debug.class.php b/system/class/Debug.class.php new file mode 100644 index 0000000..fbcabfe --- /dev/null +++ b/system/class/Debug.class.php @@ -0,0 +1,9 @@ +sql = $sql; + $this->sql = trim($sql); } - + + /** + * + * @param String $sql + * @param Mixed N additional parameters to be bound into $sql + * @return SQL + */ static public function bind( $sql ) { -//TODO $q = new SQL( $sql ); + + $argc = func_num_args(); + if( $argc > 1 ) + { + $args = func_get_args(); + array_shift( $args ); // we don't want the $sql query, just the args to bind + $q->bindArray( $args ); + } + return $q; } - - public function setDatabase( $db ) + + /** + * Bind N values (accepts n arguments) + * @param Mixed N arguments + */ + public function bindVal() { -//TODO + foreach( func_get_args() as $arg ) + $this->args[] = $arg; } - - public function getDatabase() + + /** + * Bind value to specific position in query + * @param uInt position + * @param Mixed value + * @throws Exception( SQL::ERR_EXPECTED_UINT ) + */ + public function bindPosition( $pos, $val ) { - return NULL; + $pos = intval($pos); + if( $pos < 0 ) + throw new Exception( SQL::ERR_EXPECTED_UINT ); + $this->args[$pos] = $val; } - - public function setMode( $mode ) + + /** + * Bind an array of values, in order, to the next available bind positions. + * Keys in the array are ignored. + * @param Array $args values + * @throws Exception( SQL::ERR_EXPECTED_ARRAY ) + */ + public function bindArray( $args ) { -//TODO + if( !is_array( $args ) ) + throw new Exception( SQL::ERR_EXPECTED_ARRAY ); + + foreach( $args as $a ) + $this->args[] = $a; } - + + /** + * Clears all values set to bind into query. + * (Useful when reusing a SQL object for multiple queries to the db) + */ + public function bindClear() + { + $this->args = array(); + } + + /** + * Capture the number of rows found for this query (automatically using SQL_CALC_FOUND_ROWS) + * @param Bool + * @see SQL::willCalcFoundRows() + */ + public function setCalcFoundRows( $calc_found_rows = TRUE ) + { + $this->calc_found_rows = $calc_found_rows; + } + + /** + * Whether this object will calculate (and cash) the result of SELECT SQL_CALC_FOUND_ROWS ... + * @return Bool + * @see SQL::setCalcFoundRows() + */ public function willCalcFoundRows() { -//TODO - return FALSE; + return $this->calc_found_rows; } - + + /** + * Returns result from SELECT FOUND_ROWS() call + * @return uInt + */ + public function getFoundRows() + { + if( !$this->calc_found_rows ) + throw new Exception( SQL::ERR_CALC_FOUND_ROWS_DISABLED ); + + return intval( $this->found_rows ); + } + + /** + * Used by DB class as a callback to pass back the result of SQL_CALC_FOUND_ROWS. + * Do not usre. This is used internally by the Ember framework. + * @param uInt $foundrows + * @access private + */ + public function setFoundRowsCallback( $foundrows ) + { + $this->found_rows = $foundrows; + } + + /** + * Hook for DB class to record query execution time in this object. + * Do not use. It is used internally by the Ember framework. + * @param Number $t + * @access private + */ + public function recordQueryTimeHook( $t ) + { + $this->query_execution_time = $t; + } + + /** + * Returns the bound query. + * Note that every time this method is called it will perform the bind again, so + * do not call this repeatedly unless that's actually what you want. + * @return String + * @throws Exception( SQL::ERR_ARG_COUNT ) + */ public function getSQL() { -//TODO - return $this->sql; + $sql = $this->sql;; + + if( preg_match_all('/\?([A-Za-z])([A-Za-z])?/', $sql, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE) ) + { + $arg_count = count($matches); + if( count($this->args) != $arg_count ) + throw new Exception( + str_replace( '%expected%', $arg_count, + str_replace( '%received%', count($this->args), + SQL::ERR_ARG_COUNT + ))); + + // Loop backward to preserve positions in the string + for( $i=$arg_count-1; $i >= 0; $i-- ) + { + if( !array_key_exists($i,$this->args) ) + throw new Exception( str_replace( '%pos%', $i, SQL::ERR_EXPECTED_ARG_AT_POS ) ); + + $arg = $this->args[$i]; + $type = $matches[$i][1][0]; + $modifier = isset($matches[$i][2][0]) ? $matches[$i][2][0] : NULL; + + $arg = SQL::dataBind( $type, $arg, $modifier, 0 ); + + $sql = substr_replace( $sql, $arg, $matches[$i][0][1], strlen($matches[$i][0][0]) ); + } + } + + // $sql contains the bound query at this point + + if( $this->calc_found_rows ) + { + // User requested the SQL_CALC_FOUND_ROWS result from the DB + if( FALSE === strpos( strtoupper( $sql ), 'SQL_CALC_FOUND_ROWS' ) ) + { + if( 'SELECT' == strtoupper( substr( $sql, 0, 6 ) ) ) + $sql = 'SELECT SQL_CALC_FOUND_ROWS' . substr( $sql, 6, strlen($sql)-6 ); + } + $sql = trim($sql); + if( FALSE === strpos( strtoupper( $sql ), 'SELECT FOUND_ROWS()' ) ) + { + if (substr($sql, -1) != ';') + $sql .= '; '; + $sql .= 'SELECT FOUND_ROWS();'; + } + } + + if( Debug::isEnabled() ) + $this->bound_query = $sql; + + return $sql; + } + + /** + * Magically turn this object into the bound query it represents + * @return String + */ + public function __toString() + { + return $this->getSQL(); + } + + /** + * @param String $type + * @param Mixed $arg + * @param String|NULL $modifier + * @param Mixed $default + * @return Mixed + */ + static public function dataBind( $type, $arg, $modifier=NULL, $default = 0, $params=NULL ) + { + $escape_style = SQL::ESCAPE_STYLE_MYSQL; + +//TODO: breakout params + + switch( $type{0} ) + { + case 'f': // mysql fieldname +//TODO: Can we autodetect mysql and sqlite from PDO when called from the DB class? + $fieldname_parts = explode( '.', $arg ); + if( count($fieldname_parts) <= 0 || count($fieldname_parts) > 3 ) + throw new Exception( str_replace( '%fieldname%', $arg, SQL::ERR_INVALID_SQL_FIELD ) ); + + foreach( $fieldname_parts as &$fp ) + { + // escape backticks (for mysql) + if( $escape_style == SQL::ESCAPE_STYLE_MYSQL ) + $fp = str_replace( '`', '``', $fp ); +//TODO: Are there any other valid characters for a fieldname? + if( 1 != preg_match( '/^[A-Za-z_`][A-Za-z0-9_`]*$/', $fp ) || strlen($fp) > SQL::SQLFIELD_MAX_LEN ) + throw new Exception( str_replace( '%fieldname%', $arg, SQL::ERR_INVALID_SQL_FIELD ) ); + switch( $escape_style ) + { + case SQL::ESCAPE_STYLE_MYSQL: + $fp = '`' . $fp . '`'; + break; + case SQL::ESCAPE_STYLE_SQLITE: + $fp = '"' . $fp . '"'; + break; + default: + throw new Exception( SQL::ERR_INTERNAL . ' ' . __LINE__ ); + } + } + $arg = implode( '.', $fieldname_parts ); + break; + case 'd': // mysql datetime (accepts unix timestamp or MySQL DATETIME) +//TODO: datetimes + break; + case 'h': // htmlspecialchars + if( $arg ) + $arg = htmlspecialchars( $arg ); + case 's': // string + $arg = ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ? 'NULL' : SQL::stringEscape( $arg, $escape_style ); + break; + case 'i': // signed integer + if( ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ) + $arg = 'NULL'; + else + { + $tmp = intval($arg); + if( (string) $tmp == (string) $arg ) + $arg = $tmp; + else if( PHP_VERSION_ID >= 50100 ) // need 5.0.5, but let's assume 5.1 + { +//TODO: optimize this check by caching it as a const when this file is loaded + if( strlen($arg) > strlen(PHP_INT_MAX)-1 ) + { + if( 1 != preg_match( '/^[-]{0,1}[0-9]+$/', (String) $arg ) ) + { + if( $default === FALSE ) + throw new Exception( str_replace( '%arg%', $arg, SQL::ERR_BIND_REQUIRED_INT ) ); + else + $arg = $default; + } + } + } + else + $arg = $tmp; // give up + } + break; + case 'u': // unsigned integer + if( ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ) + $arg = 'NULL'; + else + { + $tmp = intval($arg); + if( (string) $tmp == (string) $arg ) + $arg = $tmp; + else if( PHP_VERSION_ID >= 50100 ) // need 5.0.5, but let's assume 5.1 + { + if( strlen($arg) > strlen(PHP_INT_MAX)-1 ) + { + if( 1 != preg_match( '/^[0-9]+$/', (String) $arg ) ) + { + if( $default === FALSE ) + throw new Exception( str_replace( '%arg%', $arg, SQL::ERR_BIND_REQUIRED_INT ) ); + else + $arg = $default; + } + } + } + else + $arg = $tmp; // give up + } + break; + case 'a': // array (for use with IN) + if( ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ) + { + // this covers bad data, false/null data, and also empty arrays + $arg = '(NULL)'; + break; + } + if( !is_array( $arg ) ) + throw new Exception( SQL::ERR_EXPECTED_ARRAY ); + foreach( $arg as &$arg_element ) + $arg_element = SQL::stringEscape( htmlspecialchars($arg_element), $escape_style ); + $arg = '(' . implode( ',', $arg ) . ')'; + break; + case 'r': // real number +//TODO: need to engineer a regex to match mysql-compatible real numbers + break; + case 'z': // serialized data + $arg = @serialize($arg); + $arg = ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ? 'NULL' : SQL::stringEscape( $arg, $escape_style ); + break; + default: + throw new Exception( str_replace( '%type%', $type, SQL::ERR_UNKNOWN_DATATYPE ) ); + } + + return $arg; + } + + /** + * Escape a string in the appropriate format + * @param String $arg (String to escape) + * @param String $escape_style (SQL::ESCAPE_STYLE_MYSQL or SQL::ESCAPE_STYLE_SQLITE) + */ + static public function stringEscape( $arg, $escape_style ) + { + switch( $escape_style ) + { + case SQL::ESCAPE_STYLE_MYSQL: + return '"' . addslashes( (String) $arg ) . '"'; + case SQL::ESCAPE_STYLE_SQLITE: + // EMBER_SQLCLASS_SQLITE_DRIVER is defined at the end of this file + switch( EMBER_SQLCLASS_SQLITE_DRIVER ) + { + case 'sqlite3': + return '\'' . SQLite3::escapeString( (String) $arg ) . '\''; + case 'sqlite': + return '\'' . sqlite_escape_string( (String) $arg ) . '\''; + default: + throw new Exception( SQL::ERR_NO_SQLITE_DRIVER ); + } + break; + default: + throw new Exception( str_replace( '%style%', $escape_style, SQL::ERR_INVALID_ESCAPE_STYLE ) ); + } } } +// define the EMBER_SQLCLASS_SQLITE_DRIVER constant for stringEscape() +if( extension_loaded('sqlite3') ) + define( 'EMBER_SQLCLASS_SQLITE_DRIVER', 'sqlite3' ); +else if( extension_loaded('sqlite') ) + define( 'EMBER_SQLCLASS_SQLITE_DRIVER', 'sqlite' ); +else + define( 'EMBER_SQLCLASS_SQLITE_DRIVER', 'none' ); + +if( !defined('PHP_VERSION_ID') ) +{ + $version = explode('.', PHP_VERSION); + define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2])); +} From d6d75cffa78c99c6f833f11ca43f762f8453f439 Mon Sep 17 00:00:00 2001 From: Matthew Pelmear Date: Thu, 29 Mar 2012 01:00:47 -0700 Subject: [PATCH 7/7] DB connector and SQL class --- system/class/DB.class.php | 9 ++---- system/class/SQL.class.php | 62 ++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/system/class/DB.class.php b/system/class/DB.class.php index 58354e0..429553c 100644 --- a/system/class/DB.class.php +++ b/system/class/DB.class.php @@ -130,13 +130,10 @@ static private function doDBcall( $method, $sql, $db ) { $mode = DB::MODE_AUTO; if( $db === NULL ) - { - if( $sql->getDatabase() === NULL ) $db = 'default'; - else $db = $sql->getDatabase(); - } + $db = 'default'; - $sql->setDatabase( DB::$connections[$db] ); - $sql->setMode( $mode ); +// $sql->setDatabase( DB::$connections[$db] ); +// $sql->setMode( $mode ); if( Debug::isEnabled() ) { diff --git a/system/class/SQL.class.php b/system/class/SQL.class.php index 4aa2fe8..4a12004 100644 --- a/system/class/SQL.class.php +++ b/system/class/SQL.class.php @@ -17,10 +17,12 @@ class SQL const ERR_INTERNAL = 'Internal error'; const ERR_UNKNOWN_DATATYPE = 'Unknown datatype during bind: "%type%"'; const ERR_INVALID_ESCAPE_STYLE = 'Invalid escape style: "%style%"'; + const ERR_INVALID_DATETIME = 'Encountered invalid datetime value with not default value specified'; const ERR_NO_SQLITE_DRIVER = 'No compatible SQLite driver available'; const ERR_EXPECTED_ARRAY = 'Expected an array'; const ERR_EXPECTED_UINT = 'Expected an unsigned integer'; const ERR_BIND_REQUIRED_INT = 'Bind required an integer, but received: "%arg%"'; + const ERR_BIND_REQUIRED_REAL = 'Bind required a real number, but received: "%arg%"'; const ERR_INVALID_SQL_FIELD = 'Encountered an invalid SQL field: "%fieldname%"'; const ERR_CALC_FOUND_ROWS_DISABLED = 'You must enabled the SQL_CALC_FOUND_ROWS feature before you can retrieve the results'; const ERR_ARG_COUNT = 'Incorrect number of arguments for query bind (expected %expected% but received %received%)'; @@ -310,8 +312,26 @@ static public function dataBind( $type, $arg, $modifier=NULL, $default = 0, $par $arg = implode( '.', $fieldname_parts ); break; case 'd': // mysql datetime (accepts unix timestamp or MySQL DATETIME) -//TODO: datetimes - break; + if( ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ) + { + $arg = 'NULL'; + break; + } + + if( (String) intval($arg) == $arg ) + $arg = date( 'Y-m-d H:i:s', $arg ); + + // YYYY-mm-dd HH:ii:ss + if( 1 != preg_match( '/^([1-3][0-9]{3,3})-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[0-1])\s([0-1][0-9]|2[0-4]):([0-5][0-9]):([0-5][0-9])$/', $arg ) ) + { + // YYYY-mm-dd + if( 1 != preg_match( '/^([1-3][0-9]{3,3})-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[0-1])$/', $arg ) ) + throw new Exception( SQL::ERR_INVALID_DATETIME ); + $arg = SQL::stringEscape( $arg, $escape_style ); + break; // validated (YYYY-MM-DD) + } + $arg = SQL::stringEscape( $arg, $escape_style ); + break; // validated (YYYY-mm-dd HH:ii:ss) case 'h': // htmlspecialchars if( $arg ) $arg = htmlspecialchars( $arg ); @@ -339,9 +359,16 @@ static public function dataBind( $type, $arg, $modifier=NULL, $default = 0, $par $arg = $default; } } + else + { + if( $default === FALSE ) + throw new Exception( str_replace( '%arg%', $arg, SQL::ERR_BIND_REQUIRED_INT ) ); + else + $arg = $default; + } } else - $arg = $tmp; // give up + throw new Exception( SQL::ERR_INTERNAL . ' ' . __LINE__ ); } break; case 'u': // unsigned integer @@ -350,7 +377,7 @@ static public function dataBind( $type, $arg, $modifier=NULL, $default = 0, $par else { $tmp = intval($arg); - if( (string) $tmp == (string) $arg ) + if( (string) $tmp == (string) $arg && $tmp > 0 ) $arg = $tmp; else if( PHP_VERSION_ID >= 50100 ) // need 5.0.5, but let's assume 5.1 { @@ -361,12 +388,19 @@ static public function dataBind( $type, $arg, $modifier=NULL, $default = 0, $par if( $default === FALSE ) throw new Exception( str_replace( '%arg%', $arg, SQL::ERR_BIND_REQUIRED_INT ) ); else - $arg = $default; + $arg = $default; } } + else + { + if( $default === FALSE ) + throw new Exception( str_replace( '%arg%', $arg, SQL::ERR_BIND_REQUIRED_INT ) ); + else + $arg = $default; + } } else - $arg = $tmp; // give up + throw new Exception( SQL::ERR_INTERNAL . ' ' . __LINE__ ); } break; case 'a': // array (for use with IN) @@ -383,11 +417,21 @@ static public function dataBind( $type, $arg, $modifier=NULL, $default = 0, $par $arg = '(' . implode( ',', $arg ) . ')'; break; case 'r': // real number -//TODO: need to engineer a regex to match mysql-compatible real numbers + if( ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ) + $arg = 'NULL'; + else + { + if( 1 != preg_match( '/^[\-]?[0-9]*[\.]?[0-9]*([eE][\-]?[0-9]*)?$/', (String) $arg ) ) + { + if( $default === FALSE ) + throw new Exception( str_replace( '%arg%', $arg, SQL::ERR_BIND_REQUIRED_REAL ) ); + else + $arg = $default; + } + } break; case 'z': // serialized data - $arg = @serialize($arg); - $arg = ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ? 'NULL' : SQL::stringEscape( $arg, $escape_style ); + $arg = ($modifier == 'n' && $arg === NULL) || ($modifier == 'N' && !$arg) ? 'NULL' : SQL::stringEscape( @serialize($arg), $escape_style ); break; default: throw new Exception( str_replace( '%type%', $type, SQL::ERR_UNKNOWN_DATATYPE ) );