diff --git a/pear/DB.php b/pear/DB.php new file mode 100644 index 0000000..69477cb --- /dev/null +++ b/pear/DB.php @@ -0,0 +1,1114 @@ + | +// | Tomas V.V.Cox | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ +// +// Database independent query interface. + + +require_once 'PEAR.php'; + +// {{{ constants +// {{{ error codes + +/* + * The method mapErrorCode in each DB_dbtype implementation maps + * native error codes to one of these. + * + * If you add an error code here, make sure you also add a textual + * version of it in DB::errorMessage(). + */ +define('DB_OK', 1); +define('DB_ERROR', -1); +define('DB_ERROR_SYNTAX', -2); +define('DB_ERROR_CONSTRAINT', -3); +define('DB_ERROR_NOT_FOUND', -4); +define('DB_ERROR_ALREADY_EXISTS', -5); +define('DB_ERROR_UNSUPPORTED', -6); +define('DB_ERROR_MISMATCH', -7); +define('DB_ERROR_INVALID', -8); +define('DB_ERROR_NOT_CAPABLE', -9); +define('DB_ERROR_TRUNCATED', -10); +define('DB_ERROR_INVALID_NUMBER', -11); +define('DB_ERROR_INVALID_DATE', -12); +define('DB_ERROR_DIVZERO', -13); +define('DB_ERROR_NODBSELECTED', -14); +define('DB_ERROR_CANNOT_CREATE', -15); +define('DB_ERROR_CANNOT_DELETE', -16); +define('DB_ERROR_CANNOT_DROP', -17); +define('DB_ERROR_NOSUCHTABLE', -18); +define('DB_ERROR_NOSUCHFIELD', -19); +define('DB_ERROR_NEED_MORE_DATA', -20); +define('DB_ERROR_NOT_LOCKED', -21); +define('DB_ERROR_VALUE_COUNT_ON_ROW', -22); +define('DB_ERROR_INVALID_DSN', -23); +define('DB_ERROR_CONNECT_FAILED', -24); +define('DB_ERROR_EXTENSION_NOT_FOUND',-25); +define('DB_ERROR_ACCESS_VIOLATION', -26); +define('DB_ERROR_NOSUCHDB', -27); +define('DB_ERROR_CONSTRAINT_NOT_NULL',-29); + + +// }}} +// {{{ prepared statement-related + + +/* + * These constants are used when storing information about prepared + * statements (using the "prepare" method in DB_dbtype). + * + * The prepare/execute model in DB is mostly borrowed from the ODBC + * extension, in a query the "?" character means a scalar parameter. + * There are two extensions though, a "&" character means an opaque + * parameter. An opaque parameter is simply a file name, the real + * data are in that file (useful for putting uploaded files into your + * database and such). The "!" char means a parameter that must be + * left as it is. + * They modify the quote behavoir: + * DB_PARAM_SCALAR (?) => 'original string quoted' + * DB_PARAM_OPAQUE (&) => 'string from file quoted' + * DB_PARAM_MISC (!) => original string + */ +define('DB_PARAM_SCALAR', 1); +define('DB_PARAM_OPAQUE', 2); +define('DB_PARAM_MISC', 3); + + +// }}} +// {{{ binary data-related + + +/* + * These constants define different ways of returning binary data + * from queries. Again, this model has been borrowed from the ODBC + * extension. + * + * DB_BINMODE_PASSTHRU sends the data directly through to the browser + * when data is fetched from the database. + * DB_BINMODE_RETURN lets you return data as usual. + * DB_BINMODE_CONVERT returns data as well, only it is converted to + * hex format, for example the string "123" would become "313233". + */ +define('DB_BINMODE_PASSTHRU', 1); +define('DB_BINMODE_RETURN', 2); +define('DB_BINMODE_CONVERT', 3); + + +// }}} +// {{{ fetch modes + + +/** + * This is a special constant that tells DB the user hasn't specified + * any particular get mode, so the default should be used. + */ +define('DB_FETCHMODE_DEFAULT', 0); + +/** + * Column data indexed by numbers, ordered from 0 and up + */ +define('DB_FETCHMODE_ORDERED', 1); + +/** + * Column data indexed by column names + */ +define('DB_FETCHMODE_ASSOC', 2); + +/** + * Column data as object properties + */ +define('DB_FETCHMODE_OBJECT', 3); + +/** + * For multi-dimensional results: normally the first level of arrays + * is the row number, and the second level indexed by column number or name. + * DB_FETCHMODE_FLIPPED switches this order, so the first level of arrays + * is the column name, and the second level the row number. + */ +define('DB_FETCHMODE_FLIPPED', 4); + +/* for compatibility */ +define('DB_GETMODE_ORDERED', DB_FETCHMODE_ORDERED); +define('DB_GETMODE_ASSOC', DB_FETCHMODE_ASSOC); +define('DB_GETMODE_FLIPPED', DB_FETCHMODE_FLIPPED); + + +// }}} +// {{{ tableInfo() && autoPrepare()-related + + +/** + * these are constants for the tableInfo-function + * they are bitwised or'ed. so if there are more constants to be defined + * in the future, adjust DB_TABLEINFO_FULL accordingly + */ +define('DB_TABLEINFO_ORDER', 1); +define('DB_TABLEINFO_ORDERTABLE', 2); +define('DB_TABLEINFO_FULL', 3); + +/* + * Used by autoPrepare() + */ +define('DB_AUTOQUERY_INSERT', 1); +define('DB_AUTOQUERY_UPDATE', 2); + + +// }}} +// {{{ portability modes + + +/** + * Portability: turn off all portability features. + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_NONE', 0); + +/** + * Portability: convert names of tables and fields to lower case + * when using the get*(), fetch*() and tableInfo() methods. + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_LOWERCASE', 1); + +/** + * Portability: right trim the data output by get*() and fetch*(). + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_RTRIM', 2); + +/** + * Portability: force reporting the number of rows deleted. + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_DELETE_COUNT', 4); + +/** + * Portability: enable hack that makes numRows() work in Oracle. + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_NUMROWS', 8); + +/** + * Portability: makes certain error messages in certain drivers compatible + * with those from other DBMS's. + * + * + mysql, mysqli: change unique/primary key constraints + * DB_ERROR_ALREADY_EXISTS -> DB_ERROR_CONSTRAINT + * + * + odbc(access): MS's ODBC driver reports 'no such field' as code + * 07001, which means 'too few parameters.' When this option is on + * that code gets mapped to DB_ERROR_NOSUCHFIELD. + * + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_ERRORS', 16); + +/** + * Portability: convert null values to empty strings in data output by + * get*() and fetch*(). + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_NULL_TO_EMPTY', 32); + +/** + * Portability: turn on all portability features. + * @see DB_common::setOption() + */ +define('DB_PORTABILITY_ALL', 63); + +// }}} + + +// }}} +// {{{ class DB + +/** + * The main "DB" class is simply a container class with some static + * methods for creating DB objects as well as some utility functions + * common to all parts of DB. + * + * The object model of DB is as follows (indentation means inheritance): + * + * DB The main DB class. This is simply a utility class + * with some "static" methods for creating DB objects as + * well as common utility functions for other DB classes. + * + * DB_common The base for each DB implementation. Provides default + * | implementations (in OO lingo virtual methods) for + * | the actual DB implementations as well as a bunch of + * | query utility functions. + * | + * +-DB_mysql The DB implementation for MySQL. Inherits DB_common. + * When calling DB::factory or DB::connect for MySQL + * connections, the object returned is an instance of this + * class. + * + * @package DB + * @author Stig Bakken + * @author Tomas V.V.Cox + * @since PHP 4.0 + * @version $Id$ + * @category Database + */ +class DB +{ + // {{{ &factory() + + /** + * Create a new DB object for the specified database type. + * + * Allows creation of a DB_ object from which the object's + * methods can be utilized without actually connecting to a database. + * + * @param string $type database type, for example "mysql" + * @param array $options associative array of option names and values + * + * @return object a new DB object. On error, an error object. + * + * @see DB_common::setOption() + * @access public + */ + function &factory($type, $options = false) + { + if (!is_array($options)) { + $options = array('persistent' => $options); + } + + if (isset($options['debug']) && $options['debug'] >= 2) { + // expose php errors with sufficient debug level + include_once "DB/{$type}.php"; + } else { + @include_once "DB/{$type}.php"; + } + + $classname = "DB_${type}"; + + if (!class_exists($classname)) { + $tmp = PEAR::raiseError(null, DB_ERROR_NOT_FOUND, null, null, + "Unable to include the DB/{$type}.php file", + 'DB_Error', true); + return $tmp; + } + + @$obj =& new $classname; + + foreach ($options as $option => $value) { + $test = $obj->setOption($option, $value); + if (DB::isError($test)) { + return $test; + } + } + + return $obj; + } + + // }}} + // {{{ &connect() + + /** + * Create a new DB object and connect to the specified database. + * + * Example 1. + * 2, + * 'portability' => DB_PORTABILITY_ALL, + * ); + * + * $dbh =& DB::connect($dsn, $options); + * if (DB::isError($dbh)) { + * die($dbh->getMessage()); + * } + * ?> + * + * @param mixed $dsn string "data source name" or an array in the + * format returned by DB::parseDSN() + * + * @param array $options an associative array of option names and + * their values + * + * @return object a newly created DB connection object, or a DB + * error object on error + * + * @see DB::parseDSN(), DB_common::setOption(), DB::isError() + * @access public + */ + function &connect($dsn, $options = array()) + { + $dsninfo = DB::parseDSN($dsn); + $type = $dsninfo['phptype']; + + if (!is_array($options)) { + /* + * For backwards compatibility. $options used to be boolean, + * indicating whether the connection should be persistent. + */ + $options = array('persistent' => $options); + } + + if (isset($options['debug']) && $options['debug'] >= 2) { + // expose php errors with sufficient debug level + include_once "DB/${type}.php"; + } else { + @include_once "DB/${type}.php"; + } + + $classname = "DB_${type}"; + if (!class_exists($classname)) { + $tmp = PEAR::raiseError(null, DB_ERROR_NOT_FOUND, null, null, + "Unable to include the DB/{$type}.php file for `$dsn'", + 'DB_Error', true); + return $tmp; + } + + @$obj =& new $classname; + + foreach ($options as $option => $value) { + $test = $obj->setOption($option, $value); + if (DB::isError($test)) { + return $test; + } + } + + $err = $obj->connect($dsninfo, $obj->getOption('persistent')); + if (DB::isError($err)) { + $err->addUserInfo($dsn); + return $err; + } + + return $obj; + } + + // }}} + // {{{ apiVersion() + + /** + * Return the DB API version + * + * @return int the DB API version number + * + * @access public + */ + function apiVersion() + { + return 2; + } + + // }}} + // {{{ isError() + + /** + * Tell whether a result code from a DB method is an error + * + * @param int $value result code + * + * @return bool whether $value is an error + * + * @access public + */ + function isError($value) + { + return is_a($value, 'DB_Error'); + } + + // }}} + // {{{ isConnection() + + /** + * Tell whether a value is a DB connection + * + * @param mixed $value value to test + * + * @return bool whether $value is a DB connection + * + * @access public + */ + function isConnection($value) + { + return (is_object($value) && + is_subclass_of($value, 'db_common') && + method_exists($value, 'simpleQuery')); + } + + // }}} + // {{{ isManip() + + /** + * Tell whether a query is a data manipulation query (insert, + * update or delete) or a data definition query (create, drop, + * alter, grant, revoke). + * + * @access public + * + * @param string $query the query + * + * @return boolean whether $query is a data manipulation query + */ + function isManip($query) + { + $manips = 'INSERT|UPDATE|DELETE|LOAD DATA|'.'REPLACE|CREATE|DROP|'. + 'ALTER|GRANT|REVOKE|'.'LOCK|UNLOCK'; + if (preg_match('/^\s*"?('.$manips.')\s+/i', $query)) { + return true; + } + return false; + } + + // }}} + // {{{ errorMessage() + + /** + * Return a textual error message for a DB error code + * + * @param integer $value error code + * + * @return string error message, or false if the error code was + * not recognized + */ + function errorMessage($value) + { + static $errorMessages; + if (!isset($errorMessages)) { + $errorMessages = array( + DB_ERROR => 'unknown error', + DB_ERROR_ALREADY_EXISTS => 'already exists', + DB_ERROR_CANNOT_CREATE => 'can not create', + DB_ERROR_CANNOT_DELETE => 'can not delete', + DB_ERROR_CANNOT_DROP => 'can not drop', + DB_ERROR_CONSTRAINT => 'constraint violation', + DB_ERROR_CONSTRAINT_NOT_NULL=> 'null value violates not-null constraint', + DB_ERROR_DIVZERO => 'division by zero', + DB_ERROR_INVALID => 'invalid', + DB_ERROR_INVALID_DATE => 'invalid date or time', + DB_ERROR_INVALID_NUMBER => 'invalid number', + DB_ERROR_MISMATCH => 'mismatch', + DB_ERROR_NODBSELECTED => 'no database selected', + DB_ERROR_NOSUCHFIELD => 'no such field', + DB_ERROR_NOSUCHTABLE => 'no such table', + DB_ERROR_NOT_CAPABLE => 'DB backend not capable', + DB_ERROR_NOT_FOUND => 'not found', + DB_ERROR_NOT_LOCKED => 'not locked', + DB_ERROR_SYNTAX => 'syntax error', + DB_ERROR_UNSUPPORTED => 'not supported', + DB_ERROR_VALUE_COUNT_ON_ROW => 'value count on row', + DB_ERROR_INVALID_DSN => 'invalid DSN', + DB_ERROR_CONNECT_FAILED => 'connect failed', + DB_OK => 'no error', + DB_ERROR_NEED_MORE_DATA => 'insufficient data supplied', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extension not found', + DB_ERROR_NOSUCHDB => 'no such database', + DB_ERROR_ACCESS_VIOLATION => 'insufficient permissions', + DB_ERROR_TRUNCATED => 'truncated' + ); + } + + if (DB::isError($value)) { + $value = $value->getCode(); + } + + return isset($errorMessages[$value]) ? $errorMessages[$value] : $errorMessages[DB_ERROR]; + } + + // }}} + // {{{ parseDSN() + + /** + * Parse a data source name. + * + * Additional keys can be added by appending a URI query string to the + * end of the DSN. + * + * The format of the supplied DSN is in its fullest form: + * + * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true + * + * + * Most variations are allowed: + * + * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644 + * phptype://username:password@hostspec/database_name + * phptype://username:password@hostspec + * phptype://username@hostspec + * phptype://hostspec/database + * phptype://hostspec + * phptype(dbsyntax) + * phptype + * + * + * @param string $dsn Data Source Name to be parsed + * + * @return array an associative array with the following keys: + * + phptype: Database backend used in PHP (mysql, odbc etc.) + * + dbsyntax: Database used with regards to SQL syntax etc. + * + protocol: Communication protocol to use (tcp, unix etc.) + * + hostspec: Host specification (hostname[:port]) + * + database: Database to use on the DBMS server + * + username: User name for login + * + password: Password for login + * + * @author Tomas V.V.Cox + */ + function parseDSN($dsn) + { + $parsed = array( + 'phptype' => false, + 'dbsyntax' => false, + 'username' => false, + 'password' => false, + 'protocol' => false, + 'hostspec' => false, + 'port' => false, + 'socket' => false, + 'database' => false, + ); + + if (is_array($dsn)) { + $dsn = array_merge($parsed, $dsn); + if (!$dsn['dbsyntax']) { + $dsn['dbsyntax'] = $dsn['phptype']; + } + return $dsn; + } + + // Find phptype and dbsyntax + if (($pos = strpos($dsn, '://')) !== false) { + $str = substr($dsn, 0, $pos); + $dsn = substr($dsn, $pos + 3); + } else { + $str = $dsn; + $dsn = null; + } + + // Get phptype and dbsyntax + // $str => phptype(dbsyntax) + if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) { + $parsed['phptype'] = $arr[1]; + $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2]; + } else { + $parsed['phptype'] = $str; + $parsed['dbsyntax'] = $str; + } + + if (!count($dsn)) { + return $parsed; + } + + // Get (if found): username and password + // $dsn => username:password@protocol+hostspec/database + if (($at = strrpos($dsn,'@')) !== false) { + $str = substr($dsn, 0, $at); + $dsn = substr($dsn, $at + 1); + if (($pos = strpos($str, ':')) !== false) { + $parsed['username'] = rawurldecode(substr($str, 0, $pos)); + $parsed['password'] = rawurldecode(substr($str, $pos + 1)); + } else { + $parsed['username'] = rawurldecode($str); + } + } + + // Find protocol and hostspec + + // $dsn => proto(proto_opts)/database + if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) { + $proto = $match[1]; + $proto_opts = $match[2] ? $match[2] : false; + $dsn = $match[3]; + + // $dsn => protocol+hostspec/database (old format) + } else { + if (strpos($dsn, '+') !== false) { + list($proto, $dsn) = explode('+', $dsn, 2); + } + if (strpos($dsn, '/') !== false) { + list($proto_opts, $dsn) = explode('/', $dsn, 2); + } else { + $proto_opts = $dsn; + $dsn = null; + } + } + + // process the different protocol options + $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp'; + $proto_opts = rawurldecode($proto_opts); + if ($parsed['protocol'] == 'tcp') { + if (strpos($proto_opts, ':') !== false) { + list($parsed['hostspec'], $parsed['port']) = explode(':', $proto_opts); + } else { + $parsed['hostspec'] = $proto_opts; + } + } elseif ($parsed['protocol'] == 'unix') { + $parsed['socket'] = $proto_opts; + } + + // Get dabase if any + // $dsn => database + if ($dsn) { + // /database + if (($pos = strpos($dsn, '?')) === false) { + $parsed['database'] = rawurldecode($dsn); + // /database?param1=value1¶m2=value2 + } else { + $parsed['database'] = rawurldecode(substr($dsn, 0, $pos)); + $dsn = substr($dsn, $pos + 1); + if (strpos($dsn, '&') !== false) { + $opts = explode('&', $dsn); + } else { // database?param1=value1 + $opts = array($dsn); + } + foreach ($opts as $opt) { + list($key, $value) = explode('=', $opt); + if (!isset($parsed[$key])) { + // don't allow params overwrite + $parsed[$key] = rawurldecode($value); + } + } + } + } + + return $parsed; + } + + // }}} + // {{{ assertExtension() + + /** + * Load a PHP database extension if it is not loaded already. + * + * @access public + * + * @param string $name the base name of the extension (without the .so or + * .dll suffix) + * + * @return boolean true if the extension was already or successfully + * loaded, false if it could not be loaded + */ + function assertExtension($name) + { + if (!extension_loaded($name)) { + $dlext = OS_WINDOWS ? '.dll' : '.so'; + $dlprefix = OS_WINDOWS ? 'php_' : ''; + @dl($dlprefix . $name . $dlext); + return extension_loaded($name); + } + return true; + } + // }}} +} + +// }}} +// {{{ class DB_Error + +/** + * DB_Error implements a class for reporting portable database error + * messages. + * + * @package DB + * @author Stig Bakken + */ +class DB_Error extends PEAR_Error +{ + // {{{ constructor + + /** + * DB_Error constructor. + * + * @param mixed $code DB error code, or string with error message. + * @param integer $mode what "error mode" to operate in + * @param integer $level what error level to use for $mode & PEAR_ERROR_TRIGGER + * @param mixed $debuginfo additional debug info, such as the last query + * + * @access public + * + * @see PEAR_Error + */ + function DB_Error($code = DB_ERROR, $mode = PEAR_ERROR_RETURN, + $level = E_USER_NOTICE, $debuginfo = null) + { + if (is_int($code)) { + $this->PEAR_Error('DB Error: ' . DB::errorMessage($code), $code, $mode, $level, $debuginfo); + } else { + $this->PEAR_Error("DB Error: $code", DB_ERROR, $mode, $level, $debuginfo); + } + } + // }}} +} + +// }}} +// {{{ class DB_result + +/** + * This class implements a wrapper for a DB result set. + * A new instance of this class will be returned by the DB implementation + * after processing a query that returns data. + * + * @package DB + * @author Stig Bakken + */ +class DB_result +{ + // {{{ properties + + var $dbh; + var $result; + var $row_counter = null; + + /** + * for limit queries, the row to start fetching + * @var integer + */ + var $limit_from = null; + + /** + * for limit queries, the number of rows to fetch + * @var integer + */ + var $limit_count = null; + + // }}} + // {{{ constructor + + /** + * DB_result constructor. + * @param resource &$dbh DB object reference + * @param resource $result result resource id + * @param array $options assoc array with optional result options + */ + function DB_result(&$dbh, $result, $options = array()) + { + $this->dbh = &$dbh; + $this->result = $result; + foreach ($options as $key => $value) { + $this->setOption($key, $value); + } + $this->limit_type = $dbh->features['limit']; + $this->autofree = $dbh->options['autofree']; + $this->fetchmode = $dbh->fetchmode; + $this->fetchmode_object_class = $dbh->fetchmode_object_class; + } + + function setOption($key, $value = null) + { + switch ($key) { + case 'limit_from': + $this->limit_from = $value; break; + case 'limit_count': + $this->limit_count = $value; break; + } + } + + // }}} + // {{{ fetchRow() + + /** + * Fetch a row of data and return it by reference into an array. + * + * The type of array returned can be controlled either by setting this + * method's $fetchmode parameter or by changing the default + * fetch mode setFetchMode() before calling this method. + * + * There are two options for standardizing the information returned + * from databases, ensuring their values are consistent when changing + * DBMS's. These portability options can be turned on when creating a + * new DB object or by using setOption(). + * + * + DB_PORTABILITY_LOWERCASE + * convert names of fields to lower case + * + * + DB_PORTABILITY_RTRIM + * right trim the data + * + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return array a row of data, null on no more rows or PEAR_Error + * object on error + * + * @see DB_common::setOption(), DB_common::setFetchMode() + * @access public + */ + function &fetchRow($fetchmode = DB_FETCHMODE_DEFAULT, $rownum=null) + { + if ($fetchmode === DB_FETCHMODE_DEFAULT) { + $fetchmode = $this->fetchmode; + } + if ($fetchmode === DB_FETCHMODE_OBJECT) { + $fetchmode = DB_FETCHMODE_ASSOC; + $object_class = $this->fetchmode_object_class; + } + if ($this->limit_from !== null) { + if ($this->row_counter === null) { + $this->row_counter = $this->limit_from; + // Skip rows + if ($this->limit_type == false) { + $i = 0; + while ($i++ < $this->limit_from) { + $this->dbh->fetchInto($this->result, $arr, $fetchmode); + } + } + } + if ($this->row_counter >= ( + $this->limit_from + $this->limit_count)) + { + if ($this->autofree) { + $this->free(); + } + $tmp = null; + return $tmp; + } + if ($this->limit_type == 'emulate') { + $rownum = $this->row_counter; + } + $this->row_counter++; + } + $res = $this->dbh->fetchInto($this->result, $arr, $fetchmode, $rownum); + if ($res === DB_OK) { + if (isset($object_class)) { + // default mode specified in DB_common::fetchmode_object_class property + if ($object_class == 'stdClass') { + $arr = (object) $arr; + } else { + $arr = &new $object_class($arr); + } + } + return $arr; + } + if ($res == null && $this->autofree) { + $this->free(); + } + return $res; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row of data into an array which is passed by reference. + * + * The type of array returned can be controlled either by setting this + * method's $fetchmode parameter or by changing the default + * fetch mode setFetchMode() before calling this method. + * + * There are two options for standardizing the information returned + * from databases, ensuring their values are consistent when changing + * DBMS's. These portability options can be turned on when creating a + * new DB object or by using setOption(). + * + * + DB_PORTABILITY_LOWERCASE + * convert names of fields to lower case + * + * + DB_PORTABILITY_RTRIM + * right trim the data + * + * @param array &$arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null on no more rows or + * a DB_Error object on error + * + * @see DB_common::setOption(), DB_common::setFetchMode() + * @access public + */ + function fetchInto(&$arr, $fetchmode = DB_FETCHMODE_DEFAULT, $rownum=null) + { + if ($fetchmode === DB_FETCHMODE_DEFAULT) { + $fetchmode = $this->fetchmode; + } + if ($fetchmode === DB_FETCHMODE_OBJECT) { + $fetchmode = DB_FETCHMODE_ASSOC; + $object_class = $this->fetchmode_object_class; + } + if ($this->limit_from !== null) { + if ($this->row_counter === null) { + $this->row_counter = $this->limit_from; + // Skip rows + if ($this->limit_type == false) { + $i = 0; + while ($i++ < $this->limit_from) { + $this->dbh->fetchInto($this->result, $arr, $fetchmode); + } + } + } + if ($this->row_counter >= ( + $this->limit_from + $this->limit_count)) + { + if ($this->autofree) { + $this->free(); + } + return null; + } + if ($this->limit_type == 'emulate') { + $rownum = $this->row_counter; + } + + $this->row_counter++; + } + $res = $this->dbh->fetchInto($this->result, $arr, $fetchmode, $rownum); + if ($res === DB_OK) { + if (isset($object_class)) { + // default mode specified in DB_common::fetchmode_object_class property + if ($object_class == 'stdClass') { + $arr = (object) $arr; + } else { + $arr = new $object_class($arr); + } + } + return DB_OK; + } + if ($res == null && $this->autofree) { + $this->free(); + } + return $res; + } + + // }}} + // {{{ numCols() + + /** + * Get the the number of columns in a result set. + * + * @return int the number of columns, or a DB error + * + * @access public + */ + function numCols() + { + return $this->dbh->numCols($this->result); + } + + // }}} + // {{{ numRows() + + /** + * Get the number of rows in a result set. + * + * @return int the number of rows, or a DB error + * + * @access public + */ + function numRows() + { + return $this->dbh->numRows($this->result); + } + + // }}} + // {{{ nextResult() + + /** + * Get the next result if a batch of queries was executed. + * + * @return bool true if a new result is available or false if not. + * + * @access public + */ + function nextResult() + { + return $this->dbh->nextResult($this->result); + } + + // }}} + // {{{ free() + + /** + * Frees the resources allocated for this result set. + * @return int error code + * + * @access public + */ + function free() + { + $err = $this->dbh->freeResult($this->result); + if (DB::isError($err)) { + return $err; + } + $this->result = false; + return true; + } + + // }}} + // {{{ tableInfo() + + /** + * @deprecated + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($mode = null) + { + if (is_string($mode)) { + return $this->dbh->raiseError(DB_ERROR_NEED_MORE_DATA); + } + return $this->dbh->tableInfo($this, $mode); + } + + // }}} + // {{{ getRowCounter() + + /** + * returns the actual row number + * @return integer + */ + function getRowCounter() + { + return $this->row_counter; + } + // }}} +} + +// }}} +// {{{ class DB_row + +/** + * Pear DB Row Object + * @see DB_common::setFetchMode() + */ +class DB_row +{ + // {{{ constructor + + /** + * constructor + * + * @param resource row data as array + */ + function DB_row(&$arr) + { + foreach ($arr as $key => $value) { + $this->$key = &$arr[$key]; + } + } + + // }}} +} + +// }}} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/common.php b/pear/DB/common.php new file mode 100644 index 0000000..868a101 --- /dev/null +++ b/pear/DB/common.php @@ -0,0 +1,2042 @@ + | +// | Tomas V.V.Cox | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'PEAR.php'; + +/** + * DB_common is a base class for DB implementations, and must be + * inherited by all such + * + * @package DB + * @version $Id$ + * @category Database + * @author Stig Bakken + * @author Tomas V.V.Cox + */ +class DB_common extends PEAR +{ + // {{{ properties + + /** + * assoc of capabilities for this DB implementation + * $features['limit'] => 'emulate' => emulate with fetch row by number + * 'alter' => alter the query + * false => skip rows + * @var array + */ + var $features = array(); + + /** + * assoc mapping native error codes to DB ones + * @var array + */ + var $errorcode_map = array(); + + /** + * DB type (mysql, oci8, odbc etc.) + * @var string + */ + var $phptype; + + /** + * @var string + */ + var $prepare_tokens; + + /** + * @var string + */ + var $prepare_types; + + /** + * @var string + */ + var $prepared_queries; + + /** + * @var integer + */ + var $prepare_maxstmt = 0; + + /** + * @var string + */ + var $last_query = ''; + + /** + * @var integer + */ + var $fetchmode = DB_FETCHMODE_ORDERED; + + /** + * @var string + */ + var $fetchmode_object_class = 'stdClass'; + + /** + * Run-time configuration options. + * + * The 'optimize' option has been deprecated. Use the 'portability' + * option instead. + * + * @see DB_common::setOption() + * @var array + */ + var $options = array( + 'persistent' => false, + 'ssl' => false, + 'debug' => 0, + 'seqname_format' => '%s_seq', + 'autofree' => false, + 'portability' => DB_PORTABILITY_NONE, + 'optimize' => 'performance', // Deprecated. Use 'portability'. + ); + + /** + * DB handle + * @var resource + */ + var $dbh; + + // }}} + // {{{ toString() + + /** + * String conversation + * + * @return string + * @access private + */ + function toString() + { + $info = strtolower(get_class($this)); + $info .= ': (phptype=' . $this->phptype . + ', dbsyntax=' . $this->dbsyntax . + ')'; + + if ($this->connection) { + $info .= ' [connected]'; + } + + return $info; + } + + // }}} + // {{{ constructor + + /** + * Constructor + */ + function DB_common() + { + $this->PEAR('DB_Error'); + } + + // }}} + // {{{ quoteString() + + /** + * DEPRECATED: Quotes a string so it can be safely used within string + * delimiters in a query + * + * @return string quoted string + * + * @see DB_common::quoteSmart(), DB_common::escapeSimple() + * @deprecated Deprecated in release 1.2 or lower + * @internal + */ + function quoteString($string) + { + $string = $this->quote($string); + if ($string{0} == "'") { + return substr($string, 1, -1); + } + return $string; + } + + // }}} + // {{{ quote() + + /** + * DEPRECATED: Quotes a string so it can be safely used in a query + * + * @param string $string the input string to quote + * + * @return string The NULL string or the string quotes + * in magic_quote_sybase style + * + * @see DB_common::quoteSmart(), DB_common::escapeSimple() + * @deprecated Deprecated in release 1.6.0 + * @internal + */ + function quote($string = null) + { + return ($string === null) ? 'NULL' : "'".str_replace("'", "''", $string)."'"; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quote a string so it can be safely used as a table or column name + * + * Delimiting style depends on which database driver is being used. + * + * NOTE: just because you CAN use delimited identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * Portability is broken by using the following characters inside + * delimited identifiers: + * + backtick (`) -- due to MySQL + * + double quote (") -- due to Oracle + * + brackets ([ or ]) -- due to Access + * + * Delimited identifiers are known to generally work correctly under + * the following drivers: + * + mssql + * + mysql + * + mysqli + * + oci8 + * + odbc(access) + * + odbc(db2) + * + pgsql + * + sqlite + * + sybase + * + * InterBase doesn't seem to be able to use delimited identifiers + * via PHP 4. They work fine under PHP 5. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @since 1.6.0 + * @access public + */ + function quoteIdentifier($str) + { + return '"' . str_replace('"', '""', $str) . '"'; + } + + // }}} + // {{{ quoteSmart() + + /** + * Format input so it can be safely used in a query + * + * The output depends on the PHP data type of input and the database + * type being used. + * + * @param mixed $in data to be quoted + * + * @return mixed the format of the results depends on the input's + * PHP type: + * + *
    + *
  • + * input -> returns + *
  • + *
  • + * null -> the string NULL + *
  • + *
  • + * integer or double -> the unquoted number + *
  • + *
  • + * &type.bool; -> output depends on the driver in use + * Most drivers return integers: 1 if + * true or 0 if + * false. + * Some return strings: TRUE if + * true or FALSE if + * false. + * Finally one returns strings: T if + * true or F if + * false. Here is a list of each DBMS, + * the values returned and the suggested column type: + *
      + *
    • + * dbase -> T/F + * (Logical) + *
    • + *
    • + * fbase -> TRUE/FALSE + * (BOOLEAN) + *
    • + *
    • + * ibase -> 1/0 + * (SMALLINT) [1] + *
    • + *
    • + * ifx -> 1/0 + * (SMALLINT) [1] + *
    • + *
    • + * msql -> 1/0 + * (INTEGER) + *
    • + *
    • + * mssql -> 1/0 + * (BIT) + *
    • + *
    • + * mysql -> 1/0 + * (TINYINT(1)) + *
    • + *
    • + * mysqli -> 1/0 + * (TINYINT(1)) + *
    • + *
    • + * oci8 -> 1/0 + * (NUMBER(1)) + *
    • + *
    • + * odbc -> 1/0 + * (SMALLINT) [1] + *
    • + *
    • + * pgsql -> TRUE/FALSE + * (BOOLEAN) + *
    • + *
    • + * sqlite -> 1/0 + * (INTEGER) + *
    • + *
    • + * sybase -> 1/0 + * (TINYINT(1)) + *
    • + *
    + * [1] Accommodate the lowest common denominator because not all + * versions of have BOOLEAN. + *
  • + *
  • + * other (including strings and numeric strings) -> + * the data with single quotes escaped by preceeding + * single quotes, backslashes are escaped by preceeding + * backslashes, then the whole string is encapsulated + * between single quotes + *
  • + *
+ * + * @since 1.6.0 + * @see DB_common::escapeSimple() + * @access public + */ + function quoteSmart($in) + { + if (is_int($in) || is_double($in)) { + return $in; + } elseif (is_bool($in)) { + return $in ? 1 : 0; + } elseif (is_null($in)) { + return 'NULL'; + } else { + return "'" . $this->escapeSimple($in) . "'"; + } + } + + // }}} + // {{{ escapeSimple() + + /** + * Escape a string according to the current DBMS's standards + * + * In SQLite, this makes things safe for inserts/updates, but may + * cause problems when performing text comparisons against columns + * containing binary data. See the + * {@link http://php.net/sqlite_escape_string PHP manual} for more info. + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @since 1.6.0 + * @see DB_common::quoteSmart() + * @access public + */ + function escapeSimple($str) { + return str_replace("'", "''", $str); + } + + // }}} + // {{{ provides() + + /** + * Tell whether a DB implementation or its backend extension + * supports a given feature + * + * @param array $feature name of the feature (see the DB class doc) + * @return bool whether this DB implementation supports $feature + * @access public + */ + function provides($feature) + { + return $this->features[$feature]; + } + + // }}} + // {{{ errorCode() + + /** + * Map native error codes to DB's portable ones + * + * Requires that the DB implementation's constructor fills + * in the $errorcode_map property. + * + * @param mixed $nativecode the native error code, as returned by the + * backend database extension (string or integer) + * + * @return int a portable DB error code, or DB_ERROR if this DB + * implementation has no mapping for the given error code. + * + * @access public + */ + function errorCode($nativecode) + { + if (isset($this->errorcode_map[$nativecode])) { + return $this->errorcode_map[$nativecode]; + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; + } + + // }}} + // {{{ errorMessage() + + /** + * Map a DB error code to a textual message. This is actually + * just a wrapper for DB::errorMessage() + * + * @param integer $dbcode the DB error code + * + * @return string the corresponding error message, of false + * if the error code was unknown + * + * @access public + */ + function errorMessage($dbcode) + { + return DB::errorMessage($this->errorcode_map[$dbcode]); + } + + // }}} + // {{{ raiseError() + + /** + * Communicate an error and invoke error callbacks, etc + * + * Basically a wrapper for PEAR::raiseError without the message string. + * + * @param mixed integer error code, or a PEAR error object (all + * other parameters are ignored if this parameter is + * an object + * + * @param int error mode, see PEAR_Error docs + * + * @param mixed If error mode is PEAR_ERROR_TRIGGER, this is the + * error level (E_USER_NOTICE etc). If error mode is + * PEAR_ERROR_CALLBACK, this is the callback function, + * either as a function name, or as an array of an + * object and method name. For other error modes this + * parameter is ignored. + * + * @param string Extra debug information. Defaults to the last + * query and native error code. + * + * @param mixed Native error code, integer or string depending the + * backend. + * + * @return object a PEAR error object + * + * @access public + * @see PEAR_Error + */ + function &raiseError($code = DB_ERROR, $mode = null, $options = null, + $userinfo = null, $nativecode = null) + { + // The error is yet a DB error object + if (is_object($code)) { + // because we the static PEAR::raiseError, our global + // handler should be used if it is set + if ($mode === null && !empty($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + } + $tmp = PEAR::raiseError($code, null, $mode, $options, null, null, true); + return $tmp; + } + + if ($userinfo === null) { + $userinfo = $this->last_query; + } + + if ($nativecode) { + $userinfo .= ' [nativecode=' . trim($nativecode) . ']'; + } + + $tmp = PEAR::raiseError(null, $code, $mode, $options, $userinfo, + 'DB_Error', true); + return $tmp; + } + + // }}} + // {{{ setFetchMode() + + /** + * Sets which fetch mode should be used by default on queries + * on this connection + * + * @param integer $fetchmode DB_FETCHMODE_ORDERED or + * DB_FETCHMODE_ASSOC, possibly bit-wise OR'ed with + * DB_FETCHMODE_FLIPPED. + * + * @param string $object_class The class of the object + * to be returned by the fetch methods when + * the DB_FETCHMODE_OBJECT mode is selected. + * If no class is specified by default a cast + * to object from the assoc array row will be done. + * There is also the posibility to use and extend the + * 'DB_row' class. + * + * @see DB_FETCHMODE_ORDERED + * @see DB_FETCHMODE_ASSOC + * @see DB_FETCHMODE_FLIPPED + * @see DB_FETCHMODE_OBJECT + * @see DB_row::DB_row() + * @access public + */ + function setFetchMode($fetchmode, $object_class = 'stdClass') + { + switch ($fetchmode) { + case DB_FETCHMODE_OBJECT: + $this->fetchmode_object_class = $object_class; + case DB_FETCHMODE_ORDERED: + case DB_FETCHMODE_ASSOC: + $this->fetchmode = $fetchmode; + break; + default: + return $this->raiseError('invalid fetchmode mode'); + } + } + + // }}} + // {{{ setOption() + + /** + * Set run-time configuration options for PEAR DB + * + * Options, their data types, default values and description: + *
    + *
  • + * autofree boolean = false + *
    should results be freed automatically when there are no + * more rows? + *
  • + * debug integer = 0 + *
    debug level + *
  • + * persistent boolean = false + *
    should the connection be persistent? + *
  • + * portability integer = DB_PORTABILITY_NONE + *
    portability mode constant (see below) + *
  • + * seqname_format string = %s_seq + *
    the sprintf() format string used on sequence names. This + * format is applied to sequence names passed to + * createSequence(), nextID() and dropSequence(). + *
  • + * ssl boolean = false + *
    use ssl to connect? + *
  • + *
+ * + * ----------------------------------------- + * + * PORTABILITY MODES + * + * These modes are bitwised, so they can be combined using | + * and removed using ^. See the examples section below on how + * to do this. + * + * DB_PORTABILITY_NONE + * turn off all portability features + * + * This mode gets automatically turned on if the deprecated + * optimize option gets set to performance. + * + * + * DB_PORTABILITY_LOWERCASE + * convert names of tables and fields to lower case when using + * get*(), fetch*() and tableInfo() + * + * This mode gets automatically turned on in the following databases + * if the deprecated option optimize gets set to + * portability: + * + oci8 + * + * + * DB_PORTABILITY_RTRIM + * right trim the data output by get*() fetch*() + * + * + * DB_PORTABILITY_DELETE_COUNT + * force reporting the number of rows deleted + * + * Some DBMS's don't count the number of rows deleted when performing + * simple DELETE FROM tablename queries. This portability + * mode tricks such DBMS's into telling the count by adding + * WHERE 1=1 to the end of DELETE queries. + * + * This mode gets automatically turned on in the following databases + * if the deprecated option optimize gets set to + * portability: + * + fbsql + * + mysql + * + mysqli + * + sqlite + * + * + * DB_PORTABILITY_NUMROWS + * enable hack that makes numRows() work in Oracle + * + * This mode gets automatically turned on in the following databases + * if the deprecated option optimize gets set to + * portability: + * + oci8 + * + * + * DB_PORTABILITY_ERRORS + * makes certain error messages in certain drivers compatible + * with those from other DBMS's + * + * + mysql, mysqli: change unique/primary key constraints + * DB_ERROR_ALREADY_EXISTS -> DB_ERROR_CONSTRAINT + * + * + odbc(access): MS's ODBC driver reports 'no such field' as code + * 07001, which means 'too few parameters.' When this option is on + * that code gets mapped to DB_ERROR_NOSUCHFIELD. + * DB_ERROR_MISMATCH -> DB_ERROR_NOSUCHFIELD + * + * + * DB_PORTABILITY_NULL_TO_EMPTY + * convert null values to empty strings in data output by get*() and + * fetch*(). Needed because Oracle considers empty strings to be null, + * while most other DBMS's know the difference between empty and null. + * + * + * DB_PORTABILITY_ALL + * turn on all portability features + * + * ----------------------------------------- + * + * Example 1. Simple setOption() example + * setOption('autofree', true); + * ?> + * + * Example 2. Portability for lowercasing and trimming + * setOption('portability', + * DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_RTRIM); + * ?> + * + * Example 3. All portability options except trimming + * setOption('portability', + * DB_PORTABILITY_ALL ^ DB_PORTABILITY_RTRIM); + * ?> + * + * @param string $option option name + * @param mixed $value value for the option + * + * @return int DB_OK on success. DB_Error object on failure. + * + * @see DB_common::$options + */ + function setOption($option, $value) + { + if (isset($this->options[$option])) { + $this->options[$option] = $value; + + /* + * Backwards compatibility check for the deprecated 'optimize' + * option. Done here in case settings change after connecting. + */ + if ($option == 'optimize') { + if ($value == 'portability') { + switch ($this->phptype) { + case 'oci8': + $this->options['portability'] = + DB_PORTABILITY_LOWERCASE | + DB_PORTABILITY_NUMROWS; + break; + case 'fbsql': + case 'mysql': + case 'mysqli': + case 'sqlite': + $this->options['portability'] = + DB_PORTABILITY_DELETE_COUNT; + break; + } + } else { + $this->options['portability'] = DB_PORTABILITY_NONE; + } + } + + return DB_OK; + } + return $this->raiseError("unknown option $option"); + } + + // }}} + // {{{ getOption() + + /** + * Returns the value of an option + * + * @param string $option option name + * + * @return mixed the option value + */ + function getOption($option) + { + if (isset($this->options[$option])) { + return $this->options[$option]; + } + return $this->raiseError("unknown option $option"); + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute() + * + * Creates a query that can be run multiple times. Each time it is run, + * the placeholders, if any, will be replaced by the contents of + * execute()'s $data argument. + * + * Three types of placeholders can be used: + * + ? scalar value (i.e. strings, integers). The system + * will automatically quote and escape the data. + * + ! value is inserted 'as is' + * + & requires a file name. The file's contents get + * inserted into the query (i.e. saving binary + * data in a db) + * + * Example 1. + * prepare('INSERT INTO tbl (a, b, c) VALUES (?, !, &)'); + * $data = array( + * "John's text", + * "'it''s good'", + * 'filename.txt' + * ); + * $res = $dbh->execute($sth, $data); + * ?> + * + * Use backslashes to escape placeholder characters if you don't want + * them to be interpreted as placeholders: + *
+     *    "UPDATE foo SET col=? WHERE col='over \& under'"
+     * 
+ * + * With some database backends, this is emulated. + * + * {@internal ibase and oci8 have their own prepare() methods.}} + * + * @param string $query query to be prepared + * + * @return mixed DB statement resource on success. DB_Error on failure. + * + * @see DB_common::execute() + * @access public + */ + function prepare($query) + { + $tokens = preg_split('/((?prepare_tokens[] = &$newtokens; + end($this->prepare_tokens); + + $k = key($this->prepare_tokens); + $this->prepare_types[$k] = $types; + $this->prepared_queries[$k] = implode(' ', $newtokens); + + return $k; + } + + // }}} + // {{{ autoPrepare() + + /** + * Automaticaly generate an insert or update query and pass it to prepare() + * + * @param string $table name of the table + * @param array $table_fields ordered array containing the fields names + * @param int $mode type of query to make (DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE) + * @param string $where in case of update queries, this string will be put after the sql WHERE statement + * @return resource handle for the query + * @see DB_common::prepare(), DB_common::buildManipSQL() + * @access public + */ + function autoPrepare($table, $table_fields, $mode = DB_AUTOQUERY_INSERT, $where = false) + { + $query = $this->buildManipSQL($table, $table_fields, $mode, $where); + return $this->prepare($query); + } + + // }}} + // {{{ autoExecute() + + /** + * Automaticaly generate an insert or update query and call prepare() + * and execute() with it + * + * @param string $table name of the table + * @param array $fields_values assoc ($key=>$value) where $key is a field name and $value its value + * @param int $mode type of query to make (DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE) + * @param string $where in case of update queries, this string will be put after the sql WHERE statement + * @return mixed a new DB_Result or a DB_Error when fail + * @see DB_common::autoPrepare(), DB_common::buildManipSQL() + * @access public + */ + function autoExecute($table, $fields_values, $mode = DB_AUTOQUERY_INSERT, $where = false) + { + $sth = $this->autoPrepare($table, array_keys($fields_values), $mode, $where); + $ret =& $this->execute($sth, array_values($fields_values)); + $this->freePrepared($sth); + return $ret; + + } + + // }}} + // {{{ buildManipSQL() + + /** + * Make automaticaly an sql query for prepare() + * + * Example : buildManipSQL('table_sql', array('field1', 'field2', 'field3'), DB_AUTOQUERY_INSERT) + * will return the string : INSERT INTO table_sql (field1,field2,field3) VALUES (?,?,?) + * NB : - This belongs more to a SQL Builder class, but this is a simple facility + * - Be carefull ! If you don't give a $where param with an UPDATE query, all + * the records of the table will be updated ! + * + * @param string $table name of the table + * @param array $table_fields ordered array containing the fields names + * @param int $mode type of query to make (DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE) + * @param string $where in case of update queries, this string will be put after the sql WHERE statement + * @return string sql query for prepare() + * @access public + */ + function buildManipSQL($table, $table_fields, $mode, $where = false) + { + if (count($table_fields) == 0) { + $this->raiseError(DB_ERROR_NEED_MORE_DATA); + } + $first = true; + switch ($mode) { + case DB_AUTOQUERY_INSERT: + $values = ''; + $names = ''; + foreach ($table_fields as $value) { + if ($first) { + $first = false; + } else { + $names .= ','; + $values .= ','; + } + $names .= $value; + $values .= '?'; + } + return "INSERT INTO $table ($names) VALUES ($values)"; + case DB_AUTOQUERY_UPDATE: + $set = ''; + foreach ($table_fields as $value) { + if ($first) { + $first = false; + } else { + $set .= ','; + } + $set .= "$value = ?"; + } + $sql = "UPDATE $table SET $set"; + if ($where) { + $sql .= " WHERE $where"; + } + return $sql; + default: + $this->raiseError(DB_ERROR_SYNTAX); + } + } + + // }}} + // {{{ execute() + + /** + * Executes a DB statement prepared with prepare() + * + * Example 1. + * prepare('INSERT INTO tbl (a, b, c) VALUES (?, !, &)'); + * $data = array( + * "John's text", + * "'it''s good'", + * 'filename.txt' + * ); + * $res =& $dbh->execute($sth, $data); + * ?> + * + * @param resource $stmt a DB statement resource returned from prepare() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return object a new DB_Result or a DB_Error when fail + * + * {@internal ibase and oci8 have their own execute() methods.}} + * + * @see DB_common::prepare() + * @access public + */ + function &execute($stmt, $data = array()) + { + $realquery = $this->executeEmulateQuery($stmt, $data); + if (DB::isError($realquery)) { + return $realquery; + } + $result = $this->simpleQuery($realquery); + + if (DB::isError($result) || $result === DB_OK) { + return $result; + } else { + $tmp =& new DB_result($this, $result); + return $tmp; + } + } + + // }}} + // {{{ executeEmulateQuery() + + /** + * Emulates the execute statement, when not supported + * + * @param resource $stmt a DB statement resource returned from execute() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a string containing the real query run when emulating + * prepare/execute. A DB error code is returned on failure. + * + * @see DB_common::execute() + * @access private + */ + function executeEmulateQuery($stmt, $data = array()) + { + $stmt = (int)$stmt; + if (!is_array($data)) { + $data = array($data); + } + + if (count($this->prepare_types[$stmt]) != count($data)) { + $this->last_query = $this->prepared_queries[$stmt]; + return $this->raiseError(DB_ERROR_MISMATCH); + } + + $realquery = $this->prepare_tokens[$stmt][0]; + + $i = 0; + foreach ($data as $value) { + if ($this->prepare_types[$stmt][$i] == DB_PARAM_SCALAR) { + $realquery .= $this->quoteSmart($value); + } elseif ($this->prepare_types[$stmt][$i] == DB_PARAM_OPAQUE) { + $fp = @fopen($value, 'rb'); + if (!$fp) { + return $this->raiseError(DB_ERROR_ACCESS_VIOLATION); + } + $realquery .= $this->quoteSmart(fread($fp, filesize($value))); + fclose($fp); + } else { + $realquery .= $value; + } + + $realquery .= $this->prepare_tokens[$stmt][++$i]; + } + + return $realquery; + } + + // }}} + // {{{ executeMultiple() + + /** + * This function does several execute() calls on the same + * statement handle + * + * $data must be an array indexed numerically + * from 0, one execute call is done for every "row" in the array. + * + * If an error occurs during execute(), executeMultiple() does not + * execute the unfinished rows, but rather returns that error. + * + * @param resource $stmt query handle from prepare() + * @param array $data numeric array containing the + * data to insert into the query + * + * @return mixed DB_OK or DB_Error + * + * @see DB_common::prepare(), DB_common::execute() + * @access public + */ + function executeMultiple($stmt, $data) + { + foreach ($data as $value) { + $res =& $this->execute($stmt, $value); + if (DB::isError($res)) { + return $res; + } + } + return DB_OK; + } + + // }}} + // {{{ freePrepared() + + /** + * Free the resource used in a prepared query + * + * @param $stmt The resurce returned by the prepare() function + * @see DB_common::prepare() + */ + function freePrepared($stmt) + { + $stmt = (int)$stmt; + // Free the internal prepared vars + if (isset($this->prepare_tokens[$stmt])) { + unset($this->prepare_tokens[$stmt]); + unset($this->prepare_types[$stmt]); + unset($this->prepared_queries[$stmt]); + return true; + } + return false; + } + + // }}} + // {{{ modifyQuery() + + /** + * This method is used by backends to alter queries for various + * reasons + * + * It is defined here to assure that all implementations + * have this method defined. + * + * @param string $query query to modify + * + * @return the new (modified) query + * + * @access private + */ + function modifyQuery($query) { + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * This method is used by backends to alter limited queries + * + * @param string $query query to modify + * @param integer $from the row to start to fetching + * @param integer $count the numbers of rows to fetch + * + * @return the new (modified) query + * + * @access private + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + return $query; + } + + // }}} + // {{{ query() + + /** + * Send a query to the database and return any results with a + * DB_result object + * + * The query string can be either a normal statement to be sent directly + * to the server OR if $params are passed the query can have + * placeholders and it will be passed through prepare() and execute(). + * + * @param string $query the SQL query or the statement to prepare + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a DB_result object or DB_OK on success, a DB + * error on failure + * + * @see DB_result, DB_common::prepare(), DB_common::execute() + * @access public + */ + function &query($query, $params = array()) + { + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + if (DB::isError($sth)) { + return $sth; + } + $ret =& $this->execute($sth, $params); + $this->freePrepared($sth); + return $ret; + } else { + $result = $this->simpleQuery($query); + if (DB::isError($result) || $result === DB_OK) { + return $result; + } else { + $tmp =& new DB_result($this, $result); + return $tmp; + } + } + } + + // }}} + // {{{ limitQuery() + + /** + * Generates a limited query + * + * @param string $query query + * @param integer $from the row to start to fetching + * @param integer $count the numbers of rows to fetch + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed a DB_Result object, DB_OK or a DB_Error + * + * @access public + */ + function &limitQuery($query, $from, $count, $params = array()) + { + $query = $this->modifyLimitQuery($query, $from, $count, $params); + if (DB::isError($query)){ + return $query; + } + $result =& $this->query($query, $params); + if (is_a($result, 'DB_result')) { + $result->setOption('limit_from', $from); + $result->setOption('limit_count', $count); + } + return $result; + } + + // }}} + // {{{ getOne() + + /** + * Fetch the first column of the first row of data returned from + * a query + * + * Takes care of doing the query and freeing the results when finished. + * + * @param string $query the SQL query + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return mixed the returned value of the query. DB_Error on failure. + * + * @access public + */ + function &getOne($query, $params = array()) + { + settype($params, 'array'); + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + if (DB::isError($sth)) { + return $sth; + } + $res =& $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res =& $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + + $err = $res->fetchInto($row, DB_FETCHMODE_ORDERED); + $res->free(); + + if ($err !== DB_OK) { + return $err; + } + + return $row[0]; + } + + // }}} + // {{{ getRow() + + /** + * Fetch the first row of data returned from a query + * + * Takes care of doing the query and freeing the results when finished. + * + * @param string $query the SQL query + * @param array $params array to be used in execution of the statement. + * Quantity of array elements must match quantity + * of placeholders in query. This function does + * NOT support scalars. + * @param int $fetchmode the fetch mode to use + * + * @return array the first row of results as an array indexed from + * 0, or a DB error code. + * + * @access public + */ + function &getRow($query, + $params = array(), + $fetchmode = DB_FETCHMODE_DEFAULT) + { + // compat check, the params and fetchmode parameters used to + // have the opposite order + if (!is_array($params)) { + if (is_array($fetchmode)) { + if ($params === null) { + $tmp = DB_FETCHMODE_DEFAULT; + } else { + $tmp = $params; + } + $params = $fetchmode; + $fetchmode = $tmp; + } elseif ($params !== null) { + $fetchmode = $params; + $params = array(); + } + } + + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + if (DB::isError($sth)) { + return $sth; + } + $res =& $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res =& $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + + $err = $res->fetchInto($row, $fetchmode); + + $res->free(); + + if ($err !== DB_OK) { + return $err; + } + + return $row; + } + + // }}} + // {{{ getCol() + + /** + * Fetch a single column from a result set and return it as an + * indexed array + * + * @param string $query the SQL query + * @param mixed $col which column to return (integer [column number, + * starting at 0] or string [column name]) + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * + * @return array an indexed array with the data from the first + * row at index 0, or a DB error code + * + * @see DB_common::query() + * @access public + */ + function &getCol($query, $col = 0, $params = array()) + { + settype($params, 'array'); + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + + if (DB::isError($sth)) { + return $sth; + } + + $res =& $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res =& $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + + $fetchmode = is_int($col) ? DB_FETCHMODE_ORDERED : DB_FETCHMODE_ASSOC; + + if (!is_array($row = $res->fetchRow($fetchmode))) { + $ret = array(); + } else { + if (!array_key_exists($col, $row)) { + $ret =& $this->raiseError(DB_ERROR_NOSUCHFIELD); + } else { + $ret = array($row[$col]); + while (is_array($row = $res->fetchRow($fetchmode))) { + $ret[] = $row[$col]; + } + } + } + + $res->free(); + + if (DB::isError($row)) { + $ret = $row; + } + + return $ret; + } + + // }}} + // {{{ getAssoc() + + /** + * Fetch the entire result set of a query and return it as an + * associative array using the first column as the key + * + * If the result set contains more than two columns, the value + * will be an array of the values from column 2-n. If the result + * set contains only two columns, the returned value will be a + * scalar with the value of the second column (unless forced to an + * array with the $force_array parameter). A DB error code is + * returned on errors. If the result set contains fewer than two + * columns, a DB_ERROR_TRUNCATED error is returned. + * + * For example, if the table "mytable" contains: + * + *
+     *  ID      TEXT       DATE
+     * --------------------------------
+     *  1       'one'      944679408
+     *  2       'two'      944679408
+     *  3       'three'    944679408
+     * 
+ * + * Then the call getAssoc('SELECT id,text FROM mytable') returns: + *
+     *   array(
+     *     '1' => 'one',
+     *     '2' => 'two',
+     *     '3' => 'three',
+     *   )
+     * 
+ * + * ...while the call getAssoc('SELECT id,text,date FROM mytable') returns: + *
+     *   array(
+     *     '1' => array('one', '944679408'),
+     *     '2' => array('two', '944679408'),
+     *     '3' => array('three', '944679408')
+     *   )
+     * 
+ * + * If the more than one row occurs with the same value in the + * first column, the last row overwrites all previous ones by + * default. Use the $group parameter if you don't want to + * overwrite like this. Example: + * + *
+     * getAssoc('SELECT category,id,name FROM mytable', false, null,
+     *          DB_FETCHMODE_ASSOC, true) returns:
+     *
+     *   array(
+     *     '1' => array(array('id' => '4', 'name' => 'number four'),
+     *                  array('id' => '6', 'name' => 'number six')
+     *            ),
+     *     '9' => array(array('id' => '4', 'name' => 'number four'),
+     *                  array('id' => '6', 'name' => 'number six')
+     *            )
+     *   )
+     * 
+ * + * Keep in mind that database functions in PHP usually return string + * values for results regardless of the database's internal type. + * + * @param string $query the SQL query + * @param boolean $force_array used only when the query returns + * exactly two columns. If true, the values + * of the returned array will be one-element + * arrays instead of scalars. + * @param mixed $params array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 placeholder for non-array + * parameters or 1 placeholder per array element. + * @param int $fetchmode the fetch mode to use + * @param boolean $group if true, the values of the returned array + * is wrapped in another array. If the same + * key value (in the first column) repeats + * itself, the values will be appended to + * this array instead of overwriting the + * existing values. + * + * @return array associative array with results from the query. + * DB Error on failure. + * + * @access public + */ + function &getAssoc($query, $force_array = false, $params = array(), + $fetchmode = DB_FETCHMODE_DEFAULT, $group = false) + { + settype($params, 'array'); + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + + if (DB::isError($sth)) { + return $sth; + } + + $res =& $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res =& $this->query($query); + } + + if (DB::isError($res)) { + return $res; + } + if ($fetchmode == DB_FETCHMODE_DEFAULT) { + $fetchmode = $this->fetchmode; + } + $cols = $res->numCols(); + + if ($cols < 2) { + $tmp =& $this->raiseError(DB_ERROR_TRUNCATED); + return $tmp; + } + + $results = array(); + + if ($cols > 2 || $force_array) { + // return array values + // XXX this part can be optimized + if ($fetchmode == DB_FETCHMODE_ASSOC) { + while (is_array($row = $res->fetchRow(DB_FETCHMODE_ASSOC))) { + reset($row); + $key = current($row); + unset($row[key($row)]); + if ($group) { + $results[$key][] = $row; + } else { + $results[$key] = $row; + } + } + } elseif ($fetchmode == DB_FETCHMODE_OBJECT) { + while ($row = $res->fetchRow(DB_FETCHMODE_OBJECT)) { + $arr = get_object_vars($row); + $key = current($arr); + if ($group) { + $results[$key][] = $row; + } else { + $results[$key] = $row; + } + } + } else { + while (is_array($row = $res->fetchRow(DB_FETCHMODE_ORDERED))) { + // we shift away the first element to get + // indices running from 0 again + $key = array_shift($row); + if ($group) { + $results[$key][] = $row; + } else { + $results[$key] = $row; + } + } + } + if (DB::isError($row)) { + $results = $row; + } + } else { + // return scalar values + while (is_array($row = $res->fetchRow(DB_FETCHMODE_ORDERED))) { + if ($group) { + $results[$row[0]][] = $row[1]; + } else { + $results[$row[0]] = $row[1]; + } + } + if (DB::isError($row)) { + $results = $row; + } + } + + $res->free(); + + return $results; + } + + // }}} + // {{{ getAll() + + /** + * Fetch all the rows returned from a query + * + * @param string $query the SQL query + * @param array $params array to be used in execution of the statement. + * Quantity of array elements must match quantity + * of placeholders in query. This function does + * NOT support scalars. + * @param int $fetchmode the fetch mode to use + * + * @return array an nested array. DB error on failure. + * + * @access public + */ + function &getAll($query, + $params = array(), + $fetchmode = DB_FETCHMODE_DEFAULT) + { + // compat check, the params and fetchmode parameters used to + // have the opposite order + if (!is_array($params)) { + if (is_array($fetchmode)) { + if ($params === null) { + $tmp = DB_FETCHMODE_DEFAULT; + } else { + $tmp = $params; + } + $params = $fetchmode; + $fetchmode = $tmp; + } elseif ($params !== null) { + $fetchmode = $params; + $params = array(); + } + } + + if (sizeof($params) > 0) { + $sth = $this->prepare($query); + + if (DB::isError($sth)) { + return $sth; + } + + $res =& $this->execute($sth, $params); + $this->freePrepared($sth); + } else { + $res =& $this->query($query); + } + + if (DB::isError($res) || $res === DB_OK) { + return $res; + } + + $results = array(); + while (DB_OK === $res->fetchInto($row, $fetchmode)) { + if ($fetchmode & DB_FETCHMODE_FLIPPED) { + foreach ($row as $key => $val) { + $results[$key][] = $val; + } + } else { + $results[] = $row; + } + } + + $res->free(); + + if (DB::isError($row)) { + $tmp =& $this->raiseError($row); + return $tmp; + } + return $results; + } + + // }}} + // {{{ autoCommit() + + /** + * enable automatic Commit + * + * @param boolean $onoff + * @return mixed DB_Error + * + * @access public + */ + function autoCommit($onoff=false) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ commit() + + /** + * starts a Commit + * + * @return mixed DB_Error + * + * @access public + */ + function commit() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ rollback() + + /** + * starts a rollback + * + * @return mixed DB_Error + * + * @access public + */ + function rollback() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ numRows() + + /** + * Returns the number of rows in a result object + * + * @param object DB_Result the result object to check + * + * @return mixed DB_Error or the number of rows + * + * @access public + */ + function numRows($result) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ affectedRows() + + /** + * Returns the affected rows of a query + * + * @return mixed DB_Error or number of rows + * + * @access public + */ + function affectedRows() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ errorNative() + + /** + * Returns an errormessage, provides by the database + * + * @return mixed DB_Error or message + * + * @access public + */ + function errorNative() + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ getSequenceName() + + /** + * Generate the name used inside the database for a sequence + * + * The createSequence() docblock contains notes about storing sequence + * names. + * + * @param string $sqn the sequence's public name + * + * @return string the sequence's name in the backend + * + * @see DB_common::createSequence(), DB_common::dropSequence(), + * DB_common::nextID(), DB_common::setOption() + * @access private + */ + function getSequenceName($sqn) + { + return sprintf($this->getOption('seqname_format'), + preg_replace('/[^a-z0-9_.]/i', '_', $sqn)); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @see DB_common::createSequence(), DB_common::dropSequence(), + * DB_common::getSequenceName() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * The name of a given sequence is determined by passing the string + * provided in the $seq_name argument through PHP's sprintf() + * function using the value from the seqname_format option as + * the sprintf()'s format argument. + * + * seqname_format is set via setOption(). + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @see DB_common::dropSequence(), DB_common::getSequenceName(), + * DB_common::nextID() + * @access public + */ + function createSequence($seq_name) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @see DB_common::createSequence(), DB_common::getSequenceName(), + * DB_common::nextID() + * @access public + */ + function dropSequence($seq_name) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set + * + * The format of the resulting array depends on which $mode + * you select. The sample output below is based on this query: + *
+     *    SELECT tblFoo.fldID, tblFoo.fldPhone, tblBar.fldId
+     *    FROM tblFoo
+     *    JOIN tblBar ON tblFoo.fldId = tblBar.fldId
+     * 
+ * + *
    + *
  • + * + * null (default) + *
    +     *   [0] => Array (
    +     *       [table] => tblFoo
    +     *       [name] => fldId
    +     *       [type] => int
    +     *       [len] => 11
    +     *       [flags] => primary_key not_null
    +     *   )
    +     *   [1] => Array (
    +     *       [table] => tblFoo
    +     *       [name] => fldPhone
    +     *       [type] => string
    +     *       [len] => 20
    +     *       [flags] =>
    +     *   )
    +     *   [2] => Array (
    +     *       [table] => tblBar
    +     *       [name] => fldId
    +     *       [type] => int
    +     *       [len] => 11
    +     *       [flags] => primary_key not_null
    +     *   )
    +     *   
    + * + *
  • + * + * DB_TABLEINFO_ORDER + * + *

    In addition to the information found in the default output, + * a notation of the number of columns is provided by the + * num_fields element while the order + * element provides an array with the column names as the keys and + * their location index number (corresponding to the keys in the + * the default output) as the values.

    + * + *

    If a result set has identical field names, the last one is + * used.

    + * + *
    +     *   [num_fields] => 3
    +     *   [order] => Array (
    +     *       [fldId] => 2
    +     *       [fldTrans] => 1
    +     *   )
    +     *   
    + * + *
  • + * + * DB_TABLEINFO_ORDERTABLE + * + *

    Similar to DB_TABLEINFO_ORDER but adds more + * dimensions to the array in which the table names are keys and + * the field names are sub-keys. This is helpful for queries that + * join tables which have identical field names.

    + * + *
    +     *   [num_fields] => 3
    +     *   [ordertable] => Array (
    +     *       [tblFoo] => Array (
    +     *           [fldId] => 0
    +     *           [fldPhone] => 1
    +     *       )
    +     *       [tblBar] => Array (
    +     *           [fldId] => 2
    +     *       )
    +     *   )
    +     *   
    + * + *
  • + *
+ * + * The flags element contains a space separated list + * of extra information about the field. This data is inconsistent + * between DBMS's due to the way each DBMS works. + * + primary_key + * + unique_key + * + multiple_key + * + not_null + * + * Most DBMS's only provide the table and flags + * elements if $result is a table name. The following DBMS's + * provide full information from queries: + * + fbsql + * + mysql + * + * If the 'portability' option has DB_PORTABILITY_LOWERCASE + * turned on, the names of tables and fields will be lowercased. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table. + * While this also accepts a query result + * resource identifier, this behavior is + * deprecated. + * @param int $mode either unused or one of the tableInfo modes: + * DB_TABLEINFO_ORDERTABLE, + * DB_TABLEINFO_ORDER or + * DB_TABLEINFO_FULL (which does both). + * These are bitwise, so the first two can be + * combined using |. + * @return array an associative array with the information requested. + * If something goes wrong an error object is returned. + * + * @see DB_common::setOption() + * @access public + */ + function tableInfo($result, $mode = null) + { + /* + * If the DB_ class has a tableInfo() method, that one + * overrides this one. But, if the driver doesn't have one, + * this method runs and tells users about that fact. + */ + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ getTables() + + /** + * @deprecated Deprecated in release 1.2 or lower + */ + function getTables() + { + return $this->getListOf('tables'); + } + + // }}} + // {{{ getListOf() + + /** + * list internal DB info + * valid values for $type are db dependent, + * often: databases, users, view, functions + * + * @param string $type type of requested info + * + * @return mixed DB_Error or the requested data + * + * @access public + */ + function getListOf($type) + { + $sql = $this->getSpecialQuery($type); + if ($sql === null) { // No support + return $this->raiseError(DB_ERROR_UNSUPPORTED); + } elseif (is_int($sql) || DB::isError($sql)) { // Previous error + return $this->raiseError($sql); + } elseif (is_array($sql)) { // Already the result + return $sql; + } + return $this->getCol($sql); // Launch this query + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * + * @param string $type What kind of info you want to retrieve + * + * @return string The SQL query string + * + * @access public + */ + function getSpecialQuery($type) + { + return $this->raiseError(DB_ERROR_UNSUPPORTED); + } + + // }}} + // {{{ _rtrimArrayValues() + + /** + * Right trim all strings in an array + * + * @param array $array the array to be trimmed (passed by reference) + * @return void + * @access private + */ + function _rtrimArrayValues(&$array) + { + foreach ($array as $key => $value) { + if (is_string($value)) { + $array[$key] = rtrim($value); + } + } + } + + // }}} + // {{{ _convertNullArrayValuesToEmpty() + + /** + * Convert all null values in an array to empty strings + * + * @param array $array the array to be de-nullified (passed by reference) + * @return void + * @access private + */ + function _convertNullArrayValuesToEmpty(&$array) + { + foreach ($array as $key => $value) { + if (is_null($value)) { + $array[$key] = ''; + } + } + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/dbase.php b/pear/DB/dbase.php new file mode 100644 index 0000000..a5ad09f --- /dev/null +++ b/pear/DB/dbase.php @@ -0,0 +1,225 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// XXX legend: +// You have to compile your PHP with the --enable-dbase option + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's dbase + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Stig Bakken + */ +class DB_dbase extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $res_row = array(); + var $result = 0; + + // }}} + // {{{ constructor + + /** + * DB_mysql constructor. + * + * @access public + */ + function DB_dbase() + { + $this->DB_common(); + $this->phptype = 'dbase'; + $this->dbsyntax = 'dbase'; + $this->features = array( + 'prepare' => false, + 'pconnect' => false, + 'transactions' => false, + 'limit' => false + ); + $this->errorcode_map = array(); + } + + // }}} + // {{{ connect() + + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('dbase')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + $this->dsn = $dsninfo; + + $ini = ini_get('track_errors'); + if ($ini) { + $conn = @dbase_open($dsninfo['database'], 0); + } else { + ini_set('track_errors', 1); + $conn = @dbase_open($dsninfo['database'], 0); + ini_set('track_errors', $ini); + } + if (!$conn) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, + null, null, strip_tags($php_errormsg)); + } + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + function disconnect() + { + $ret = @dbase_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ &query() + + function &query($query = null) + { + // emulate result resources + $this->res_row[(int)$this->result] = 0; + $tmp =& new DB_result($this, $this->result++); + return $tmp; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum === null) { + $rownum = $this->res_row[(int)$result]++; + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @dbase_get_record_with_names($this->connection, $rownum); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @dbase_get_record($this->connection, $rownum); + } + if (!$arr) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ numCols() + + function numCols($foo) + { + return @dbase_numfields($this->connection); + } + + // }}} + // {{{ numRows() + + function numRows($foo) + { + return @dbase_numrecords($this->connection); + } + + // }}} + // {{{ quoteSmart() + + /** + * Format input so it can be safely used in a query + * + * @param mixed $in data to be quoted + * + * @return mixed Submitted variable's type = returned value: + * + null = the string NULL + * + boolean = T if true or + * F if false. Use the Logical + * data type. + * + integer or double = the unquoted number + * + other (including strings and numeric strings) = + * the data with single quotes escaped by preceeding + * single quotes then the whole string is encapsulated + * between single quotes + * + * @internal + */ + function quoteSmart($in) + { + if (is_int($in) || is_double($in)) { + return $in; + } elseif (is_bool($in)) { + return $in ? 'T' : 'F'; + } elseif (is_null($in)) { + return 'NULL'; + } else { + return "'" . $this->escapeSimple($in) . "'"; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/fbsql.php b/pear/DB/fbsql.php new file mode 100644 index 0000000..398fff6 --- /dev/null +++ b/pear/DB/fbsql.php @@ -0,0 +1,655 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// XXX legend: +// +// XXX ERRORMSG: The error message from the fbsql function should +// be registered here. +// +// TODO/wishlist: +// longReadlen +// binmode + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's FrontBase + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Frank M. Kromann + */ +class DB_fbsql extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $num_rows = array(); + var $fetchmode = DB_FETCHMODE_ORDERED; /* Default fetch mode */ + + // }}} + // {{{ constructor + + /** + * DB_fbsql constructor. + * + * @access public + */ + function DB_fbsql() + { + $this->DB_common(); + $this->phptype = 'fbsql'; + $this->dbsyntax = 'fbsql'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => true, + 'limit' => 'emulate' + ); + $this->errorcode_map = array( + 1004 => DB_ERROR_CANNOT_CREATE, + 1005 => DB_ERROR_CANNOT_CREATE, + 1006 => DB_ERROR_CANNOT_CREATE, + 1007 => DB_ERROR_ALREADY_EXISTS, + 1008 => DB_ERROR_CANNOT_DROP, + 1046 => DB_ERROR_NODBSELECTED, + 1050 => DB_ERROR_ALREADY_EXISTS, + 1051 => DB_ERROR_NOSUCHTABLE, + 1054 => DB_ERROR_NOSUCHFIELD, + 1062 => DB_ERROR_ALREADY_EXISTS, + 1064 => DB_ERROR_SYNTAX, + 1100 => DB_ERROR_NOT_LOCKED, + 1136 => DB_ERROR_VALUE_COUNT_ON_ROW, + 1146 => DB_ERROR_NOSUCHTABLE, + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * @access public + * @return int DB_OK on success, a DB error on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('fbsql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + $dbhost = $dsninfo['hostspec'] ? $dsninfo['hostspec'] : 'localhost'; + + $php_errormsg = ''; + $connect_function = $persistent ? 'fbsql_pconnect' : 'fbsql_connect'; + + if ($dbhost && $dsninfo['username'] && $dsninfo['password']) { + $conn = @$connect_function($dbhost, $dsninfo['username'], + $dsninfo['password']); + } elseif ($dbhost && $dsninfo['username']) { + $conn = @$connect_function($dbhost, $dsninfo['username']); + } elseif ($dbhost) { + $conn = @$connect_function($dbhost); + } else { + $conn = false; + } + if (!$conn) { + if (empty($php_errormsg)) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $php_errormsg); + } + } + + if ($dsninfo['database']) { + if (!fbsql_select_db($dsninfo['database'], $conn)) { + return $this->fbsqlRaiseError(); + } + } + + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @access public + * + * @return bool true on success, false if not connected. + */ + function disconnect() + { + $ret = @fbsql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to fbsql and return the results as a fbsql resource + * identifier. + * + * @param the SQL query + * + * @access public + * + * @return mixed returns a valid fbsql result for successful SELECT + * queries, DB_OK for other successful queries. A DB error is + * returned on failure. + */ + function simpleQuery($query) + { + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @fbsql_query("$query;", $this->connection); + if (!$result) { + return $this->fbsqlRaiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + if (DB::isManip($query)) { + return DB_OK; + } + $numrows = $this->numrows($result); + if (is_object($numrows)) { + return $numrows; + } + $this->num_rows[(int)$result] = $numrows; + return $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal fbsql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return @fbsql_next_result($result); + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@fbsql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @fbsql_fetch_array($result, FBSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @fbsql_fetch_row($result); + } + if (!$arr) { + $errno = @fbsql_errno($this->connection); + if (!$errno) { + return null; + } + return $this->fbsqlRaiseError($errno); + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result fbsql result identifier + * + * @access public + * + * @return bool true on success, false if $result is invalid + */ + function freeResult($result) + { + return @fbsql_free_result($result); + } + + // }}} + // {{{ autoCommit() + + function autoCommit($onoff=false) + { + if ($onoff) { + $this->query("SET COMMIT TRUE"); + } else { + $this->query("SET COMMIT FALSE"); + } + } + + // }}} + // {{{ commit() + + function commit() + { + @fbsql_commit(); + } + + // }}} + // {{{ rollback() + + function rollback() + { + @fbsql_rollback(); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result fbsql result identifier + * + * @access public + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + $cols = @fbsql_num_fields($result); + + if (!$cols) { + return $this->fbsqlRaiseError(); + } + + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Get the number of rows in a result set. + * + * @param $result fbsql result identifier + * + * @access public + * + * @return int the number of rows in $result + */ + function numRows($result) + { + $rows = @fbsql_num_rows($result); + if ($rows === null) { + return $this->fbsqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the data manipulation + * query. For other queries, this function returns 0. + * + * @return number of rows affected by the last query + */ + function affectedRows() + { + if (DB::isManip($this->last_query)) { + $result = @fbsql_affected_rows($this->connection); + } else { + $result = 0; + } + return $result; + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error code of the last error (if any) that + * occured on the current connection. + * + * @access public + * + * @return int native fbsql error code + */ + function errorNative() + { + return @fbsql_errno($this->connection); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = 0; + do { + $result = $this->query("INSERT INTO ${seqname} (id) VALUES (NULL)"); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $result; + } + } else { + $repeat = 0; + } + } while ($repeat); + if (DB::isError($result)) { + return $result; + } + return @fbsql_insert_id($this->connection); + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("CREATE TABLE ${seqname} ". + '(id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,'. + ' PRIMARY KEY(id))'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP TABLE ${seqname} RESTRICT"); + } + + // }}} + // {{{ modifyQuery() + + function modifyQuery($query) + { + if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) { + // "DELETE FROM table" gives 0 affected rows in fbsql. + // This little hack lets you know how many rows were deleted. + if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) { + $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/', + 'DELETE FROM \1 WHERE 1=1', $query); + } + } + return $query; + } + + // }}} + // {{{ quoteSmart() + + /** + * Format input so it can be safely used in a query + * + * @param mixed $in data to be quoted + * + * @return mixed Submitted variable's type = returned value: + * + null = the string NULL + * + boolean = string TRUE or FALSE + * + integer or double = the unquoted number + * + other (including strings and numeric strings) = + * the data escaped according to MySQL's settings + * then encapsulated between single quotes + * + * @internal + */ + function quoteSmart($in) + { + if (is_int($in) || is_double($in)) { + return $in; + } elseif (is_bool($in)) { + return $in ? 'TRUE' : 'FALSE'; + } elseif (is_null($in)) { + return 'NULL'; + } else { + return "'" . $this->escapeSimple($in) . "'"; + } + } + + // }}} + // {{{ fbsqlRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see DB_common::errorCode() + * @see DB_common::raiseError() + */ + function fbsqlRaiseError($errno = null) + { + if ($errno === null) { + $errno = $this->errorCode(fbsql_errno($this->connection)); + } + return $this->raiseError($errno, null, null, null, + @fbsql_error($this->connection)); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @fbsql_list_fields($this->dsn['database'], + $result, $this->connection); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->fbsqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @fbsql_num_fields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $case_func(@fbsql_field_table($id, $i)); + $res[$i]['name'] = $case_func(@fbsql_field_name($id, $i)); + $res[$i]['type'] = @fbsql_field_type($id, $i); + $res[$i]['len'] = @fbsql_field_len($id, $i); + $res[$i]['flags'] = @fbsql_field_flags($id, $i); + } + } else { // full + $res["num_fields"]= $count; + + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $case_func(@fbsql_field_table($id, $i)); + $res[$i]['name'] = $case_func(@fbsql_field_name($id, $i)); + $res[$i]['type'] = @fbsql_field_type($id, $i); + $res[$i]['len'] = @fbsql_field_len($id, $i); + $res[$i]['flags'] = @fbsql_field_flags($id, $i); + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @fbsql_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'select "table_name" from information_schema.tables'; + default: + return null; + } + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/ibase.php b/pear/DB/ibase.php new file mode 100644 index 0000000..e2eddb0 --- /dev/null +++ b/pear/DB/ibase.php @@ -0,0 +1,784 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// Bugs: +// - If dbsyntax is not firebird, the limitQuery may fail + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's Interbase + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Sterling Hughes + */ +class DB_ibase extends DB_common +{ + + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $autocommit = 1; + var $manip_query = array(); + + // }}} + // {{{ constructor + + function DB_ibase() + { + $this->DB_common(); + $this->phptype = 'ibase'; + $this->dbsyntax = 'ibase'; + $this->features = array( + 'prepare' => true, + 'pconnect' => true, + 'transactions' => true, + 'limit' => false + ); + // just a few of the tons of Interbase error codes listed in the + // Language Reference section of the Interbase manual + $this->errorcode_map = array( + -104 => DB_ERROR_SYNTAX, + -150 => DB_ERROR_ACCESS_VIOLATION, + -151 => DB_ERROR_ACCESS_VIOLATION, + -155 => DB_ERROR_NOSUCHTABLE, + 88 => DB_ERROR_NOSUCHTABLE, + -157 => DB_ERROR_NOSUCHFIELD, + -158 => DB_ERROR_VALUE_COUNT_ON_ROW, + -170 => DB_ERROR_MISMATCH, + -171 => DB_ERROR_MISMATCH, + -172 => DB_ERROR_INVALID, + -204 => DB_ERROR_INVALID, + -205 => DB_ERROR_NOSUCHFIELD, + -206 => DB_ERROR_NOSUCHFIELD, + -208 => DB_ERROR_INVALID, + -219 => DB_ERROR_NOSUCHTABLE, + -297 => DB_ERROR_CONSTRAINT, + -530 => DB_ERROR_CONSTRAINT, + -607 => DB_ERROR_NOSUCHTABLE, + -803 => DB_ERROR_CONSTRAINT, + -551 => DB_ERROR_ACCESS_VIOLATION, + -552 => DB_ERROR_ACCESS_VIOLATION, + -922 => DB_ERROR_NOSUCHDB, + -923 => DB_ERROR_CONNECT_FAILED, + -924 => DB_ERROR_CONNECT_FAILED + ); + } + + // }}} + // {{{ connect() + + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('interbase')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + $this->dsn = $dsninfo; + $dbhost = $dsninfo['hostspec'] ? + ($dsninfo['hostspec'] . ':' . $dsninfo['database']) : + $dsninfo['database']; + + $connect_function = $persistent ? 'ibase_pconnect' : 'ibase_connect'; + + $params = array(); + $params[] = $dbhost; + $params[] = $dsninfo['username'] ? $dsninfo['username'] : null; + $params[] = $dsninfo['password'] ? $dsninfo['password'] : null; + $params[] = isset($dsninfo['charset']) ? $dsninfo['charset'] : null; + $params[] = isset($dsninfo['buffers']) ? $dsninfo['buffers'] : null; + $params[] = isset($dsninfo['dialect']) ? $dsninfo['dialect'] : null; + $params[] = isset($dsninfo['role']) ? $dsninfo['role'] : null; + + $conn = @call_user_func_array($connect_function, $params); + if (!$conn) { + return $this->ibaseRaiseError(DB_ERROR_CONNECT_FAILED); + } + $this->connection = $conn; + if ($this->dsn['dbsyntax'] == 'firebird') { + $this->features['limit'] = 'alter'; + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + function disconnect() + { + $ret = @ibase_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @ibase_query($this->connection, $query); + if (!$result) { + return $this->ibaseRaiseError(); + } + if ($this->autocommit && $ismanip) { + @ibase_commit($this->connection); + } + // Determine which queries that should return data, and which + // should return an error code only. + return $ismanip ? DB_OK : $result; + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * This method is used by backends to alter limited queries + * Uses the new FIRST n SKIP n Firebird 1.0 syntax, so it is + * only compatible with Firebird 1.x + * + * @param string $query query to modify + * @param integer $from the row to start to fetching + * @param integer $count the numbers of rows to fetch + * + * @return the new (modified) query + * @author Ludovico Magnocavallo + * @access private + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if ($this->dsn['dbsyntax'] == 'firebird') { + //$from++; // SKIP starts from 1, ie SKIP 1 starts from the first record + // (cox) Seems that SKIP starts in 0 + $query = preg_replace('/^\s*select\s(.*)$/is', + "SELECT FIRST $count SKIP $from $1", $query); + } + return $query; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal ibase result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + return $this->ibaseRaiseError(DB_ERROR_NOT_CAPABLE); + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + if (function_exists('ibase_fetch_assoc')) { + $arr = @ibase_fetch_assoc($result); + } else { + $arr = get_object_vars(ibase_fetch_object($result)); + } + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @ibase_fetch_row($result); + } + if (!$arr) { + if ($errmsg = @ibase_errmsg()) { + return $this->ibaseRaiseError(null, $errmsg); + } else { + return null; + } + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + function freeResult($result) + { + return @ibase_free_result($result); + } + + // }}} + // {{{ freeQuery() + + function freeQuery($query) + { + @ibase_free_query($query); + return true; + } + + // }}} + // {{{ numCols() + + function numCols($result) + { + $cols = @ibase_num_fields($result); + if (!$cols) { + return $this->ibaseRaiseError(); + } + return $cols; + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute(). + * + * prepare() requires a generic query as string like + * INSERT INTO numbers VALUES (?, ?, ?) + * . The ? characters are placeholders. + * + * Three types of placeholders can be used: + * + ? a quoted scalar value, i.e. strings, integers + * + ! value is inserted 'as is' + * + & requires a file name. The file's contents get + * inserted into the query (i.e. saving binary + * data in a db) + * + * Use backslashes to escape placeholder characters if you don't want + * them to be interpreted as placeholders. Example: + * "UPDATE foo SET col=? WHERE col='over \& under'" + * + * + * @param string $query query to be prepared + * @return mixed DB statement resource on success. DB_Error on failure. + */ + function prepare($query) + { + $tokens = preg_split('/((? $val) { + switch ($val) { + case '?': + $types[$token++] = DB_PARAM_SCALAR; + break; + case '&': + $types[$token++] = DB_PARAM_OPAQUE; + break; + case '!': + $types[$token++] = DB_PARAM_MISC; + break; + default: + $tokens[$key] = preg_replace('/\\\([&?!])/', "\\1", $val); + $newquery .= $tokens[$key] . '?'; + } + } + + $newquery = substr($newquery, 0, -1); + $this->last_query = $query; + $newquery = $this->modifyQuery($newquery); + $stmt = @ibase_prepare($this->connection, $newquery); + $this->prepare_types[(int)$stmt] = $types; + $this->manip_query[(int)$stmt] = DB::isManip($query); + return $stmt; + } + + // }}} + // {{{ execute() + + /** + * Executes a DB statement prepared with prepare(). + * + * @param resource $stmt a DB statement resource returned from prepare() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 for non-array items or the + * quantity of elements in the array. + * @return object a new DB_Result or a DB_Error when fail + * @see DB_ibase::prepare() + * @access public + */ + function &execute($stmt, $data = array()) + { + if (!is_array($data)) { + $data = array($data); + } + + $types =& $this->prepare_types[(int)$stmt]; + if (count($types) != count($data)) { + $tmp =& $this->raiseError(DB_ERROR_MISMATCH); + return $tmp; + } + + $i = 0; + foreach ($data as $key => $value) { + if ($types[$i] == DB_PARAM_MISC) { + /* + * ibase doesn't seem to have the ability to pass a + * parameter along unchanged, so strip off quotes from start + * and end, plus turn two single quotes to one single quote, + * in order to avoid the quotes getting escaped by + * ibase and ending up in the database. + */ + $data[$key] = preg_replace("/^'(.*)'$/", "\\1", $data[$key]); + $data[$key] = str_replace("''", "'", $data[$key]); + } elseif ($types[$i] == DB_PARAM_OPAQUE) { + $fp = @fopen($data[$key], 'rb'); + if (!$fp) { + $tmp =& $this->raiseError(DB_ERROR_ACCESS_VIOLATION); + return $tmp; + } + $data[$key] = fread($fp, filesize($data[$key])); + fclose($fp); + } + $i++; + } + + array_unshift($data, $stmt); + + $res = call_user_func_array('ibase_execute', $data); + if (!$res) { + $tmp =& $this->ibaseRaiseError(); + return $tmp; + } + /* XXX need this? + if ($this->autocommit && $this->manip_query[(int)$stmt]) { + @ibase_commit($this->connection); + }*/ + if ($this->manip_query[(int)$stmt]) { + $tmp = DB_OK; + } else { + $tmp =& new DB_result($this, $res); + } + return $tmp; + } + + /** + * Free the internal resources associated with a prepared query. + * + * @param $stmt The interbase_query resource type + * + * @return bool true on success, false if $result is invalid + */ + function freePrepared($stmt) + { + if (!is_resource($stmt)) { + return false; + } + @ibase_free_query($stmt); + unset($this->prepare_tokens[(int)$stmt]); + unset($this->prepare_types[(int)$stmt]); + unset($this->manip_query[(int)$stmt]); + return true; + } + + // }}} + // {{{ autoCommit() + + function autoCommit($onoff = false) + { + $this->autocommit = $onoff ? 1 : 0; + return DB_OK; + } + + // }}} + // {{{ commit() + + function commit() + { + return @ibase_commit($this->connection); + } + + // }}} + // {{{ rollback() + + function rollback() + { + return @ibase_rollback($this->connection); + } + + // }}} + // {{{ transactionInit() + + function transactionInit($trans_args = 0) + { + return $trans_args ? @ibase_trans($trans_args, $this->connection) : @ibase_trans(); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $sqn = strtoupper($this->getSequenceName($seq_name)); + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result =& $this->query("SELECT GEN_ID(${sqn}, 1) " + . 'FROM RDB$GENERATORS ' + . "WHERE RDB\$GENERATOR_NAME='${sqn}'"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result)) { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $result; + } + } else { + $repeat = 0; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + $result->free(); + return $arr[0]; + } + + // }}} + // {{{ createSequence() + + /** + * Create the sequence + * + * @param string $seq_name the name of the sequence + * @return mixed DB_OK on success or DB error on error + * @access public + */ + function createSequence($seq_name) + { + $sqn = strtoupper($this->getSequenceName($seq_name)); + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("CREATE GENERATOR ${sqn}"); + $this->popErrorHandling(); + + return $result; + } + + // }}} + // {{{ dropSequence() + + /** + * Drop a sequence + * + * @param string $seq_name the name of the sequence + * @return mixed DB_OK on success or DB error on error + * @access public + */ + function dropSequence($seq_name) + { + $sqn = strtoupper($this->getSequenceName($seq_name)); + return $this->query('DELETE FROM RDB$GENERATORS ' + . "WHERE RDB\$GENERATOR_NAME='${sqn}'"); + } + + // }}} + // {{{ _ibaseFieldFlags() + + /** + * get the Flags of a Field + * + * @param string $field_name the name of the field + * @param string $table_name the name of the table + * + * @return string The flags of the field ("primary_key", "unique_key", "not_null" + * "default", "computed" and "blob" are supported) + * @access private + */ + function _ibaseFieldFlags($field_name, $table_name) + { + $sql = 'SELECT R.RDB$CONSTRAINT_TYPE CTYPE' + .' FROM RDB$INDEX_SEGMENTS I' + .' JOIN RDB$RELATION_CONSTRAINTS R ON I.RDB$INDEX_NAME=R.RDB$INDEX_NAME' + .' WHERE I.RDB$FIELD_NAME=\'' . $field_name . '\'' + .' AND UPPER(R.RDB$RELATION_NAME)=\'' . strtoupper($table_name) . '\''; + + $result = @ibase_query($this->connection, $sql); + if (!$result) { + return $this->ibaseRaiseError(); + } + + $flags = ''; + if ($obj = @ibase_fetch_object($result)) { + @ibase_free_result($result); + if (isset($obj->CTYPE) && trim($obj->CTYPE) == 'PRIMARY KEY') { + $flags .= 'primary_key '; + } + if (isset($obj->CTYPE) && trim($obj->CTYPE) == 'UNIQUE') { + $flags .= 'unique_key '; + } + } + + $sql = 'SELECT R.RDB$NULL_FLAG AS NFLAG,' + .' R.RDB$DEFAULT_SOURCE AS DSOURCE,' + .' F.RDB$FIELD_TYPE AS FTYPE,' + .' F.RDB$COMPUTED_SOURCE AS CSOURCE' + .' FROM RDB$RELATION_FIELDS R ' + .' JOIN RDB$FIELDS F ON R.RDB$FIELD_SOURCE=F.RDB$FIELD_NAME' + .' WHERE UPPER(R.RDB$RELATION_NAME)=\'' . strtoupper($table_name) . '\'' + .' AND R.RDB$FIELD_NAME=\'' . $field_name . '\''; + + $result = @ibase_query($this->connection, $sql); + if (!$result) { + return $this->ibaseRaiseError(); + } + if ($obj = @ibase_fetch_object($result)) { + @ibase_free_result($result); + if (isset($obj->NFLAG)) { + $flags .= 'not_null '; + } + if (isset($obj->DSOURCE)) { + $flags .= 'default '; + } + if (isset($obj->CSOURCE)) { + $flags .= 'computed '; + } + if (isset($obj->FTYPE) && $obj->FTYPE == 261) { + $flags .= 'blob '; + } + } + + return trim($flags); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * NOTE: only supports 'table' and 'flags' if $result + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @ibase_query($this->connection, + "SELECT * FROM $result WHERE 1=0"); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->ibaseRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @ibase_num_fields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + for ($i=0; $i<$count; $i++) { + $info = @ibase_field_info($id, $i); + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func($info['name']); + $res[$i]['type'] = $info['type']; + $res[$i]['len'] = $info['length']; + $res[$i]['flags'] = ($got_string) ? $this->_ibaseFieldFlags($info['name'], $result) : ''; + } + } else { // full + $res['num_fields']= $count; + + for ($i=0; $i<$count; $i++) { + $info = @ibase_field_info($id, $i); + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func($info['name']); + $res[$i]['type'] = $info['type']; + $res[$i]['len'] = $info['length']; + $res[$i]['flags'] = ($got_string) ? $this->_ibaseFieldFlags($info['name'], $result) : ''; + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @ibase_free_result($id); + } + return $res; + } + + // }}} + // {{{ ibaseRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $db_errno PEAR error number (usually a DB constant) if + * manually raising an error + * @param string $native_errmsg text of error message if known + * @return object DB error object + * @see DB_common::errorCode() + * @see DB_common::raiseError() + */ + function &ibaseRaiseError($db_errno = null, $native_errmsg = null) + { + if ($native_errmsg === null) { + $native_errmsg = @ibase_errmsg(); + } + // memo for the interbase php module hackers: we need something similar + // to mysql_errno() to retrieve error codes instead of this ugly hack + if (preg_match('/^([^0-9\-]+)([0-9\-]+)\s+(.*)$/', $native_errmsg, $m)) { + $native_errno = (int)$m[2]; + } else { + $native_errno = null; + } + // try to map the native error to the DB one + if ($db_errno === null) { + if ($native_errno) { + // try to interpret Interbase error code (that's why we need ibase_errno() + // in the interbase module to return the real error code) + switch ($native_errno) { + case -204: + if (is_int(strpos($m[3], 'Table unknown'))) { + $db_errno = DB_ERROR_NOSUCHTABLE; + } + break; + default: + $db_errno = $this->errorCode($native_errno); + } + } else { + $error_regexps = array( + '/[tT]able not found/' => DB_ERROR_NOSUCHTABLE, + '/[tT]able .* already exists/' => DB_ERROR_ALREADY_EXISTS, + '/validation error for column .* value "\*\*\* null/' => DB_ERROR_CONSTRAINT_NOT_NULL, + '/violation of [\w ]+ constraint/' => DB_ERROR_CONSTRAINT, + '/conversion error from string/' => DB_ERROR_INVALID_NUMBER, + '/no permission for/' => DB_ERROR_ACCESS_VIOLATION, + '/arithmetic exception, numeric overflow, or string truncation/' => DB_ERROR_DIVZERO + ); + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $native_errmsg)) { + $db_errno = $code; + $native_errno = null; + break; + } + } + } + } + $tmp =& $this->raiseError($db_errno, null, null, null, $native_errmsg); + return $tmp; + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/ifx.php b/pear/DB/ifx.php new file mode 100644 index 0000000..d317a3a --- /dev/null +++ b/pear/DB/ifx.php @@ -0,0 +1,579 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// Legend: +// For more info on Informix errors see: +// http://www.informix.com/answers/english/ierrors.htm +// +// TODO: +// - set needed env Informix vars on connect +// - implement native prepare/execute + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's Informix + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Tomas V.V.Cox + */ +class DB_ifx extends DB_common +{ + // {{{ properties + + var $connection; + var $affected = 0; + var $dsn = array(); + var $transaction_opcount = 0; + var $autocommit = true; + var $fetchmode = DB_FETCHMODE_ORDERED; /* Default fetch mode */ + + // }}} + // {{{ constructor + + function DB_ifx() + { + $this->phptype = 'ifx'; + $this->dbsyntax = 'ifx'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => true, + 'limit' => 'emulate' + ); + $this->errorcode_map = array( + '-201' => DB_ERROR_SYNTAX, + '-206' => DB_ERROR_NOSUCHTABLE, + '-217' => DB_ERROR_NOSUCHFIELD, + '-239' => DB_ERROR_CONSTRAINT, + '-253' => DB_ERROR_SYNTAX, + '-292' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-310' => DB_ERROR_ALREADY_EXISTS, + '-329' => DB_ERROR_NODBSELECTED, + '-346' => DB_ERROR_CONSTRAINT, + '-386' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-391' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-554' => DB_ERROR_SYNTAX, + '-691' => DB_ERROR_CONSTRAINT, + '-703' => DB_ERROR_CONSTRAINT_NOT_NULL, + '-1204' => DB_ERROR_INVALID_DATE, + '-1205' => DB_ERROR_INVALID_DATE, + '-1206' => DB_ERROR_INVALID_DATE, + '-1209' => DB_ERROR_INVALID_DATE, + '-1210' => DB_ERROR_INVALID_DATE, + '-1212' => DB_ERROR_INVALID_DATE, + '-1213' => DB_ERROR_INVALID_NUMBER, + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * + * @return int DB_OK on success, a DB error code on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('informix') && + !DB::assertExtension('Informix')) + { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + $this->dsn = $dsninfo; + $dbhost = $dsninfo['hostspec'] ? '@' . $dsninfo['hostspec'] : ''; + $dbname = $dsninfo['database'] ? $dsninfo['database'] . $dbhost : ''; + $user = $dsninfo['username'] ? $dsninfo['username'] : ''; + $pw = $dsninfo['password'] ? $dsninfo['password'] : ''; + + $connect_function = $persistent ? 'ifx_pconnect' : 'ifx_connect'; + + $this->connection = @$connect_function($dbname, $user, $pw); + if (!is_resource($this->connection)) { + return $this->ifxraiseError(DB_ERROR_CONNECT_FAILED); + } + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @return bool true on success, false if not connected. + */ + function disconnect() + { + $ret = @ifx_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to Informix and return the results as a + * Informix resource identifier. + * + * @param $query the SQL query + * + * @return int returns a valid Informix result for successful SELECT + * queries, DB_OK for other successful queries. A DB error code + * is returned on failure. + */ + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + $this->affected = null; + if (preg_match('/(SELECT)/i', $query)) { //TESTME: Use !DB::isManip()? + // the scroll is needed for fetching absolute row numbers + // in a select query result + $result = @ifx_query($query, $this->connection, IFX_SCROLL); + } else { + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @ifx_query('BEGIN WORK', $this->connection); + if (!$result) { + return $this->ifxraiseError(); + } + } + $this->transaction_opcount++; + } + $result = @ifx_query($query, $this->connection); + } + if (!$result) { + return $this->ifxraiseError(); + } + $this->affected = @ifx_affected_rows($result); + // Determine which queries should return data, and which + // should return an error code only. + if (preg_match('/(SELECT)/i', $query)) { + return $result; + } + // XXX Testme: free results inside a transaction + // may cause to stop it and commit the work? + + // Result has to be freed even with a insert or update + @ifx_free_result($result); + + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal ifx result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the last query. + * if the last query was a select, returns 0. + * + * @return number of rows affected by the last query + */ + function affectedRows() + { + if (DB::isManip($this->last_query)) { + return $this->affected; + } else { + return 0; + } + + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if (($rownum !== null) && ($rownum < 0)) { + return null; + } + if ($rownum === null) { + /* + * Even though fetch_row() should return the next row if + * $rownum is null, it doesn't in all cases. Bug 598. + */ + $rownum = 'NEXT'; + } else { + // Index starts at row 1, unlike most DBMS's starting at 0. + $rownum++; + } + if (!$arr = @ifx_fetch_row($result, $rownum)) { + return null; + } + if ($fetchmode !== DB_FETCHMODE_ASSOC) { + $i=0; + $order = array(); + foreach ($arr as $val) { + $order[$i++] = $val; + } + $arr = $order; + } elseif ($fetchmode == DB_FETCHMODE_ASSOC && + $this->options['portability'] & DB_PORTABILITY_LOWERCASE) + { + $arr = array_change_key_case($arr, CASE_LOWER); + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ numRows() + + function numRows($result) + { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result Informix result identifier + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + if (!$cols = @ifx_num_fields($result)) { + return $this->ifxraiseError(); + } + return $cols; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result Informix result identifier + * + * @return bool true on success, false if $result is invalid + */ + function freeResult($result) + { + return @ifx_free_result($result); + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits + */ + function autoCommit($onoff = true) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the current transaction. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + $result = @ifx_query('COMMIT WORK', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->ifxRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back (undo) the current transaction. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + $result = @ifx_query('ROLLBACK WORK', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->ifxRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ ifxraiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see errorNative() + * @see errorCode() + * @see DB_common::raiseError() + */ + function ifxraiseError($errno = null) + { + if ($errno === null) { + $errno = $this->errorCode(ifx_error()); + } + + return $this->raiseError($errno, null, null, null, + $this->errorNative()); + } + + // }}} + // {{{ errorCode() + + /** + * Map native error codes to DB's portable ones. + * + * Requires that the DB implementation's constructor fills + * in the $errorcode_map property. + * + * @param string $nativecode error code returned by the database + * @return int a portable DB error code, or DB_ERROR if this DB + * implementation has no mapping for the given error code. + */ + function errorCode($nativecode) + { + if (ereg('SQLCODE=(.*)]', $nativecode, $match)) { + $code = $match[1]; + if (isset($this->errorcode_map[$code])) { + return $this->errorcode_map[$code]; + } + } + return DB_ERROR; + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error message of the last error (if any) that + * occured on the current connection. + * + * @return int native Informix error code + */ + function errorNative() + { + return @ifx_error() . ' ' . @ifx_errormsg(); + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'select tabname from systables where tabid >= 100'; + default: + return null; + } + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * NOTE: only supports 'table' if $result is a table name. + * + * If analyzing a query result and the result has duplicate field names, + * an error will be raised saying + * can't distinguish duplicate field names. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @since 1.6.0 + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @ifx_query("SELECT * FROM $result WHERE 1=0", + $this->connection); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->ifxRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + $flds = @ifx_fieldproperties($id); + $count = @ifx_num_fields($id); + + if (count($flds) != $count) { + return $this->raiseError("can't distinguish duplicate field names"); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $i = 0; + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + foreach ($flds as $key => $value) { + $props = explode(';', $value); + + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func($key); + $res[$i]['type'] = $props[0]; + $res[$i]['len'] = $props[1]; + $res[$i]['flags'] = $props[4] == 'N' ? 'not_null' : ''; + $i++; + } + + } else { // full + $res['num_fields'] = $count; + + foreach ($flds as $key => $value) { + $props = explode(';', $value); + + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func($key); + $res[$i]['type'] = $props[0]; + $res[$i]['len'] = $props[1]; + $res[$i]['flags'] = $props[4] == 'N' ? 'not_null' : ''; + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + $i++; + } + } + + // free the result only if we were called on a table + if ($got_string) { + @ifx_free_result($id); + } + return $res; + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/msql.php b/pear/DB/msql.php new file mode 100644 index 0000000..c135b32 --- /dev/null +++ b/pear/DB/msql.php @@ -0,0 +1,242 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's Mini-SQL + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Sterling Hughes + */ +class DB_msql extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + + // }}} + // {{{ constructor + + function DB_msql() + { + $this->DB_common(); + $this->phptype = 'msql'; + $this->dbsyntax = 'msql'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => false, + 'limit' => 'emulate' + ); + } + + // }}} + // {{{ connect() + + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('msql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + $dbhost = $dsninfo['hostspec'] ? $dsninfo['hostspec'] : 'localhost'; + + $connect_function = $persistent ? 'msql_pconnect' : 'msql_connect'; + + if ($dbhost && $dsninfo['username'] && $dsninfo['password']) { + $conn = $connect_function($dbhost, $dsninfo['username'], + $dsninfo['password']); + } elseif ($dbhost && $dsninfo['username']) { + $conn = $connect_function($dbhost, $dsninfo['username']); + } else { + $conn = $connect_function($dbhost); + } + if (!$conn) { + $this->raiseError(DB_ERROR_CONNECT_FAILED); + } + if (!@msql_select_db($dsninfo['database'], $conn)){ + return $this->raiseError(DB_ERROR_NODBSELECTED); + } + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + function disconnect() + { + $ret = @msql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + function simpleQuery($query) + { + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @msql_query($query, $this->connection); + if (!$result) { + return $this->raiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + return DB::isManip($query) ? DB_OK : $result; + } + + + // }}} + // {{{ nextResult() + + /** + * Move the internal msql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@msql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @msql_fetch_array($result, MSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @msql_fetch_row($result); + } + if (!$arr) { + if ($error = @msql_error()) { + return $this->raiseError($error); + } else { + return null; + } + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + function freeResult($result) + { + return @msql_free_result($result); + } + + // }}} + // {{{ numCols() + + function numCols($result) + { + $cols = @msql_num_fields($result); + if (!$cols) { + return $this->raiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + function numRows($result) + { + $rows = @msql_num_rows($result); + if (!$rows) { + return $this->raiseError(); + } + return $rows; + } + + // }}} + // {{{ affected() + + /** + * Gets the number of rows affected by a query. + * + * @return number of rows affected by the last query + */ + function affectedRows() + { + return @msql_affected_rows($this->connection); + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/mssql.php b/pear/DB/mssql.php new file mode 100644 index 0000000..5bb843a --- /dev/null +++ b/pear/DB/mssql.php @@ -0,0 +1,738 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's Microsoft SQL Server + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Sterling Hughes + */ +class DB_mssql extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $transaction_opcount = 0; + var $autocommit = true; + var $_db = null; + + // }}} + // {{{ constructor + + function DB_mssql() + { + $this->DB_common(); + $this->phptype = 'mssql'; + $this->dbsyntax = 'mssql'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => true, + 'limit' => 'emulate' + ); + // XXX Add here error codes ie: 'S100E' => DB_ERROR_SYNTAX + $this->errorcode_map = array( + 170 => DB_ERROR_SYNTAX, + 207 => DB_ERROR_NOSUCHFIELD, + 208 => DB_ERROR_NOSUCHTABLE, + 245 => DB_ERROR_INVALID_NUMBER, + 515 => DB_ERROR_CONSTRAINT_NOT_NULL, + 547 => DB_ERROR_CONSTRAINT, + 2627 => DB_ERROR_CONSTRAINT, + 2714 => DB_ERROR_ALREADY_EXISTS, + 3701 => DB_ERROR_NOSUCHTABLE, + 8134 => DB_ERROR_DIVZERO, + ); + } + + // }}} + // {{{ connect() + + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('mssql') && !DB::assertExtension('sybase') + && !DB::assertExtension('sybase_ct')) + { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + $this->dsn = $dsninfo; + $dbhost = $dsninfo['hostspec'] ? $dsninfo['hostspec'] : 'localhost'; + $dbhost .= $dsninfo['port'] ? ',' . $dsninfo['port'] : ''; + + $connect_function = $persistent ? 'mssql_pconnect' : 'mssql_connect'; + + if ($dbhost && $dsninfo['username'] && $dsninfo['password']) { + $conn = @$connect_function($dbhost, $dsninfo['username'], + $dsninfo['password']); + } elseif ($dbhost && $dsninfo['username']) { + $conn = @$connect_function($dbhost, $dsninfo['username']); + } else { + $conn = @$connect_function($dbhost); + } + if (!$conn) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, @mssql_get_last_message()); + } + if ($dsninfo['database']) { + if (!@mssql_select_db($dsninfo['database'], $conn)) { + return $this->raiseError(DB_ERROR_NODBSELECTED, null, null, + null, @mssql_get_last_message()); + } + $this->_db = $dsninfo['database']; + } + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + function disconnect() + { + $ret = @mssql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $query = $this->modifyQuery($query); + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @mssql_query('BEGIN TRAN', $this->connection); + if (!$result) { + return $this->mssqlRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @mssql_query($query, $this->connection); + if (!$result) { + return $this->mssqlRaiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + return $ismanip ? DB_OK : $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal mssql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return @mssql_next_result($result); + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@mssql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @mssql_fetch_array($result, MSSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @mssql_fetch_row($result); + } + if (!$arr) { + /* This throws informative error messages, + don't use it for now + if ($msg = @mssql_get_last_message()) { + return $this->raiseError($msg); + } + */ + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + function freeResult($result) + { + return @mssql_free_result($result); + } + + // }}} + // {{{ numCols() + + function numCols($result) + { + $cols = @mssql_num_fields($result); + if (!$cols) { + return $this->mssqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + function numRows($result) + { + $rows = @mssql_num_rows($result); + if ($rows === false) { + return $this->mssqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the current transaction. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @mssql_query('COMMIT TRAN', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mssqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back (undo) the current transaction. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @mssql_query('ROLLBACK TRAN', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mssqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the last query. + * if the last query was a select, returns 0. + * + * @return number of rows affected by the last query or DB_ERROR + */ + function affectedRows() + { + if (DB::isManip($this->last_query)) { + $res = @mssql_query('select @@rowcount', $this->connection); + if (!$res) { + return $this->mssqlRaiseError(); + } + $ar = @mssql_fetch_row($res); + if (!$ar) { + $result = 0; + } else { + @mssql_free_result($res); + $result = $ar[0]; + } + } else { + $result = 0; + } + return $result; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE)) + { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } elseif (!DB::isError($result)) { + $result =& $this->query("SELECT @@IDENTITY FROM $seqname"); + $repeat = 0; + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $result = $result->fetchRow(DB_FETCHMODE_ORDERED); + return $result[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("CREATE TABLE $seqname ". + '([id] [int] IDENTITY (1, 1) NOT NULL ,' . + '[vapor] [int] NULL)'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP TABLE $seqname"); + } + + // }}} + // {{{ errorNative() + + /** + * Determine MS SQL Server error code by querying @@ERROR. + * + * @return mixed mssql's native error code or DB_ERROR if unknown. + */ + function errorNative() + { + $res = @mssql_query('select @@ERROR as ErrorCode', $this->connection); + if (!$res) { + return DB_ERROR; + } + $row = @mssql_fetch_row($res); + return $row[0]; + } + + // }}} + // {{{ errorCode() + + /** + * Determine PEAR::DB error code from mssql's native codes. + * + * If $nativecode isn't known yet, it will be looked up. + * + * @param mixed $nativecode mssql error code, if known + * @return integer an error number from a DB error constant + * @see errorNative() + */ + function errorCode($nativecode = null) + { + if (!$nativecode) { + $nativecode = $this->errorNative(); + } + if (isset($this->errorcode_map[$nativecode])) { + return $this->errorcode_map[$nativecode]; + } else { + return DB_ERROR; + } + } + + // }}} + // {{{ mssqlRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $code PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see errorCode() + * @see errorNative() + * @see DB_common::raiseError() + */ + function mssqlRaiseError($code = null) + { + $message = @mssql_get_last_message(); + if (!$code) { + $code = $this->errorNative(); + } + return $this->raiseError($this->errorCode($code), null, null, null, + "$code - $message"); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * NOTE: only supports 'table' and 'flags' if $result + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + if (!@mssql_select_db($this->_db, $this->connection)) { + return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED); + } + $id = @mssql_query("SELECT * FROM $result WHERE 1=0", + $this->connection); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->mssqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @mssql_num_fields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func(@mssql_field_name($id, $i)); + $res[$i]['type'] = @mssql_field_type($id, $i); + $res[$i]['len'] = @mssql_field_length($id, $i); + // We only support flags for tables + $res[$i]['flags'] = $got_string ? $this->_mssql_field_flags($result, $res[$i]['name']) : ''; + } + + } else { // full + $res['num_fields']= $count; + + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func(@mssql_field_name($id, $i)); + $res[$i]['type'] = @mssql_field_type($id, $i); + $res[$i]['len'] = @mssql_field_length($id, $i); + // We only support flags for tables + $res[$i]['flags'] = $got_string ? $this->_mssql_field_flags($result, $res[$i]['name']) : ''; + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @mssql_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return "select name from sysobjects where type = 'U' order by name"; + case 'views': + return "select name from sysobjects where type = 'V'"; + default: + return null; + } + } + + // }}} + // {{{ _mssql_field_flags() + + /** + * Get the flags for a field, currently supports "not_null", "primary_key", + * "auto_increment" (mssql identity), "timestamp" (mssql timestamp), + * "unique_key" (mssql unique index, unique check or primary_key) and + * "multiple_key" (multikey index) + * + * mssql timestamp is NOT similar to the mysql timestamp so this is maybe + * not useful at all - is the behaviour of mysql_field_flags that primary + * keys are alway unique? is the interpretation of multiple_key correct? + * + * @param string The table name + * @param string The field + * @author Joern Barthel + * @access private + */ + function _mssql_field_flags($table, $column) + { + static $tableName = null; + static $flags = array(); + + if ($table != $tableName) { + + $flags = array(); + $tableName = $table; + + // get unique and primary keys + $res = $this->getAll("EXEC SP_HELPINDEX[$table]", DB_FETCHMODE_ASSOC); + + foreach ($res as $val) { + $keys = explode(', ', $val['index_keys']); + + if (sizeof($keys) > 1) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'multiple_key'); + } + } + + if (strpos($val['index_description'], 'primary key')) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'primary_key'); + } + } elseif (strpos($val['index_description'], 'unique')) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'unique_key'); + } + } + } + + // get auto_increment, not_null and timestamp + $res = $this->getAll("EXEC SP_COLUMNS[$table]", DB_FETCHMODE_ASSOC); + + foreach ($res as $val) { + $val = array_change_key_case($val, CASE_LOWER); + if ($val['nullable'] == '0') { + $this->_add_flag($flags[$val['column_name']], 'not_null'); + } + if (strpos($val['type_name'], 'identity')) { + $this->_add_flag($flags[$val['column_name']], 'auto_increment'); + } + if (strpos($val['type_name'], 'timestamp')) { + $this->_add_flag($flags[$val['column_name']], 'timestamp'); + } + } + } + + if (array_key_exists($column, $flags)) { + return(implode(' ', $flags[$column])); + } + return ''; + } + + // }}} + // {{{ _add_flag() + + /** + * Adds a string to the flags array if the flag is not yet in there + * - if there is no flag present the array is created. + * + * @param reference Reference to the flag-array + * @param value The flag value + * @access private + * @author Joern Barthel + */ + function _add_flag(&$array, $value) + { + if (!is_array($array)) { + $array = array($value); + } elseif (!in_array($value, $array)) { + array_push($array, $value); + } + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quote a string so it can be safely used as a table / column name + * + * Quoting style depends on which database driver is being used. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @since 1.6.0 + * @access public + */ + function quoteIdentifier($str) + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/mysql.php b/pear/DB/mysql.php new file mode 100644 index 0000000..481b453 --- /dev/null +++ b/pear/DB/mysql.php @@ -0,0 +1,916 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// XXX legend: +// +// XXX ERRORMSG: The error message from the mysql function should +// be registered here. +// +// TODO/wishlist: +// longReadlen +// binmode + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's MySQL + * extension. + * + * This is for MySQL versions 4.0 and below. + * + * @package DB + * @version $Id$ + * @category Database + * @author Stig Bakken + */ +class DB_mysql extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $num_rows = array(); + var $transaction_opcount = 0; + var $autocommit = true; + var $fetchmode = DB_FETCHMODE_ORDERED; /* Default fetch mode */ + var $_db = false; + + // }}} + // {{{ constructor + + /** + * DB_mysql constructor. + * + * @access public + */ + function DB_mysql() + { + $this->DB_common(); + $this->phptype = 'mysql'; + $this->dbsyntax = 'mysql'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => true, + 'limit' => 'alter' + ); + $this->errorcode_map = array( + 1004 => DB_ERROR_CANNOT_CREATE, + 1005 => DB_ERROR_CANNOT_CREATE, + 1006 => DB_ERROR_CANNOT_CREATE, + 1007 => DB_ERROR_ALREADY_EXISTS, + 1008 => DB_ERROR_CANNOT_DROP, + 1022 => DB_ERROR_ALREADY_EXISTS, + 1046 => DB_ERROR_NODBSELECTED, + 1048 => DB_ERROR_CONSTRAINT, + 1050 => DB_ERROR_ALREADY_EXISTS, + 1051 => DB_ERROR_NOSUCHTABLE, + 1054 => DB_ERROR_NOSUCHFIELD, + 1062 => DB_ERROR_ALREADY_EXISTS, + 1064 => DB_ERROR_SYNTAX, + 1100 => DB_ERROR_NOT_LOCKED, + 1136 => DB_ERROR_VALUE_COUNT_ON_ROW, + 1146 => DB_ERROR_NOSUCHTABLE, + 1216 => DB_ERROR_CONSTRAINT, + 1217 => DB_ERROR_CONSTRAINT, + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * @access public + * @return int DB_OK on success, a DB error on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('mysql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + $this->dsn = $dsninfo; + if ($dsninfo['protocol'] && $dsninfo['protocol'] == 'unix') { + $dbhost = ':' . $dsninfo['socket']; + } else { + $dbhost = $dsninfo['hostspec'] ? $dsninfo['hostspec'] : 'localhost'; + if ($dsninfo['port']) { + $dbhost .= ':' . $dsninfo['port']; + } + } + + $connect_function = $persistent ? 'mysql_pconnect' : 'mysql_connect'; + + if ($dbhost && $dsninfo['username'] && isset($dsninfo['password'])) { + $conn = @$connect_function($dbhost, $dsninfo['username'], + $dsninfo['password']); + } elseif ($dbhost && $dsninfo['username']) { + $conn = @$connect_function($dbhost, $dsninfo['username']); + } elseif ($dbhost) { + $conn = @$connect_function($dbhost); + } else { + $conn = false; + } + if (!$conn) { + if (($err = @mysql_error()) != '') { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $err); + } elseif (empty($php_errormsg)) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $php_errormsg); + } + } + + if ($dsninfo['database']) { + if (!@mysql_select_db($dsninfo['database'], $conn)) { + switch(mysql_errno($conn)) { + case 1049: + return $this->raiseError(DB_ERROR_NOSUCHDB, null, null, + null, @mysql_error($conn)); + case 1044: + return $this->raiseError(DB_ERROR_ACCESS_VIOLATION, null, null, + null, @mysql_error($conn)); + default: + return $this->raiseError(DB_ERROR, null, null, + null, @mysql_error($conn)); + } + } + // fix to allow calls to different databases in the same script + $this->_db = $dsninfo['database']; + } + + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @access public + * + * @return bool true on success, false if not connected. + */ + function disconnect() + { + $ret = @mysql_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to MySQL and return the results as a MySQL resource + * identifier. + * + * @param the SQL query + * + * @access public + * + * @return mixed returns a valid MySQL result for successful SELECT + * queries, DB_OK for other successful queries. A DB error is + * returned on failure. + */ + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @mysql_query('SET AUTOCOMMIT=0', $this->connection); + $result = @mysql_query('BEGIN', $this->connection); + if (!$result) { + return $this->mysqlRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @mysql_query($query, $this->connection); + if (!$result) { + return $this->mysqlRaiseError(); + } + if (is_resource($result)) { + $numrows = $this->numrows($result); + if (is_object($numrows)) { + return $numrows; + } + $this->num_rows[(int)$result] = $numrows; + return $result; + } + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal mysql result pointer to the next available result + * + * This method has not been implemented yet. + * + * @param a valid sql result resource + * + * @access public + * + * @return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@mysql_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @mysql_fetch_array($result, MYSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @mysql_fetch_row($result); + } + if (!$arr) { + // See: http://bugs.php.net/bug.php?id=22328 + // for why we can't check errors on fetching + return null; + /* + $errno = @mysql_errno($this->connection); + if (!$errno) { + return null; + } + return $this->mysqlRaiseError($errno); + */ + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + /* + * Even though this DBMS already trims output, we do this because + * a field might have intentional whitespace at the end that + * gets removed by DB_PORTABILITY_RTRIM under another driver. + */ + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result MySQL result identifier + * + * @access public + * + * @return bool true on success, false if $result is invalid + */ + function freeResult($result) + { + unset($this->num_rows[(int)$result]); + return @mysql_free_result($result); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result MySQL result identifier + * + * @access public + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + $cols = @mysql_num_fields($result); + + if (!$cols) { + return $this->mysqlRaiseError(); + } + + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Get the number of rows in a result set. + * + * @param $result MySQL result identifier + * + * @access public + * + * @return int the number of rows in $result + */ + function numRows($result) + { + $rows = @mysql_num_rows($result); + if ($rows === null) { + return $this->mysqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the current transaction. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysql_query('COMMIT', $this->connection); + $result = @mysql_query('SET AUTOCOMMIT=1', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back (undo) the current transaction. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysql_select_db($this->_db, $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysql_query('ROLLBACK', $this->connection); + $result = @mysql_query('SET AUTOCOMMIT=1', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the data manipulation + * query. For other queries, this function returns 0. + * + * @return number of rows affected by the last query + */ + function affectedRows() + { + if (DB::isManip($this->last_query)) { + return @mysql_affected_rows($this->connection); + } else { + return 0; + } + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error code of the last error (if any) that + * occured on the current connection. + * + * @access public + * + * @return int native MySQL error code + */ + function errorNative() + { + return @mysql_errno($this->connection); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("UPDATE ${seqname} ". + 'SET id=LAST_INSERT_ID(id+1)'); + $this->popErrorHandling(); + if ($result === DB_OK) { + /** COMMON CASE **/ + $id = @mysql_insert_id($this->connection); + if ($id != 0) { + return $id; + } + /** EMPTY SEQ TABLE **/ + // Sequence table must be empty for some reason, so fill it and return 1 + // Obtain a user-level lock + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + if ($result == 0) { + // Failed to get the lock, bail with a DB_ERROR_NOT_LOCKED error + return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED); + } + + // add the default value + $result = $this->query("REPLACE INTO ${seqname} (id) VALUES (0)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + + // Release the lock + $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + // We know what the result will be, so no need to try again + return 1; + + /** ONDEMAND TABLE CREATION **/ + } elseif ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) + { + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } else { + $repeat = 1; + } + + /** BACKWARDS COMPAT **/ + } elseif (DB::isError($result) && + $result->getCode() == DB_ERROR_ALREADY_EXISTS) + { + // see _BCsequence() comment + $result = $this->_BCsequence($seqname); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $repeat = 1; + } + } while ($repeat); + + return $this->raiseError($result); + } + + // }}} + // {{{ createSequence() + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $res = $this->query("CREATE TABLE ${seqname} ". + '(id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,'. + ' PRIMARY KEY(id))'); + if (DB::isError($res)) { + return $res; + } + // insert yields value 1, nextId call will generate ID 2 + $res = $this->query("INSERT INTO ${seqname} (id) VALUES (0)"); + if (DB::isError($res)) { + return $res; + } + // so reset to zero + return $this->query("UPDATE ${seqname} SET id = 0;"); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ _BCsequence() + + /** + * Backwards compatibility with old sequence emulation implementation + * (clean up the dupes) + * + * @param string $seqname The sequence name to clean up + * @return mixed DB_Error or true + */ + function _BCsequence($seqname) + { + // Obtain a user-level lock... this will release any previous + // application locks, but unlike LOCK TABLES, it does not abort + // the current transaction and is much less frequently used. + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $result; + } + if ($result == 0) { + // Failed to get the lock, can't do the conversion, bail + // with a DB_ERROR_NOT_LOCKED error + return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED); + } + + $highest_id = $this->getOne("SELECT MAX(id) FROM ${seqname}"); + if (DB::isError($highest_id)) { + return $highest_id; + } + // This should kill all rows except the highest + // We should probably do something if $highest_id isn't + // numeric, but I'm at a loss as how to handle that... + $result = $this->query("DELETE FROM ${seqname} WHERE id <> $highest_id"); + if (DB::isError($result)) { + return $result; + } + + // If another thread has been waiting for this lock, + // it will go thru the above procedure, but will have no + // real effect + $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')"); + if (DB::isError($result)) { + return $result; + } + return true; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quote a string so it can be safely used as a table or column name + * + * Quoting style depends on which database driver is being used. + * + * MySQL can't handle the backtick character (`) in + * table or column names. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @since 1.6.0 + * @access public + * @internal + */ + function quoteIdentifier($str) + { + return '`' . $str . '`'; + } + + // }}} + // {{{ quote() + + /** + * @deprecated Deprecated in release 1.6.0 + * @internal + */ + function quote($str) { + return $this->quoteSmart($str); + } + + // }}} + // {{{ escapeSimple() + + /** + * Escape a string according to the current DBMS's standards + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @internal + */ + function escapeSimple($str) { + if (function_exists('mysql_real_escape_string')) { + return @mysql_real_escape_string($str, $this->connection); + } else { + return @mysql_escape_string($str); + } + } + + // }}} + // {{{ modifyQuery() + + function modifyQuery($query) + { + if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) { + // "DELETE FROM table" gives 0 affected rows in MySQL. + // This little hack lets you know how many rows were deleted. + if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) { + $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/', + 'DELETE FROM \1 WHERE 1=1', $query); + } + } + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if (DB::isManip($query)) { + return $query . " LIMIT $count"; + } else { + return $query . " LIMIT $from, $count"; + } + } + + // }}} + // {{{ mysqlRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see DB_common::errorCode() + * @see DB_common::raiseError() + */ + function mysqlRaiseError($errno = null) + { + if ($errno === null) { + if ($this->options['portability'] & DB_PORTABILITY_ERRORS) { + $this->errorcode_map[1022] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT_NOT_NULL; + $this->errorcode_map[1062] = DB_ERROR_CONSTRAINT; + } else { + // Doing this in case mode changes during runtime. + $this->errorcode_map[1022] = DB_ERROR_ALREADY_EXISTS; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1062] = DB_ERROR_ALREADY_EXISTS; + } + $errno = $this->errorCode(mysql_errno($this->connection)); + } + return $this->raiseError($errno, null, null, null, + @mysql_errno($this->connection) . ' ** ' . + @mysql_error($this->connection)); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @mysql_list_fields($this->dsn['database'], + $result, $this->connection); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->mysqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @mysql_num_fields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $case_func(@mysql_field_table($id, $i)); + $res[$i]['name'] = $case_func(@mysql_field_name($id, $i)); + $res[$i]['type'] = @mysql_field_type($id, $i); + $res[$i]['len'] = @mysql_field_len($id, $i); + $res[$i]['flags'] = @mysql_field_flags($id, $i); + } + } else { // full + $res['num_fields']= $count; + + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $case_func(@mysql_field_table($id, $i)); + $res[$i]['name'] = $case_func(@mysql_field_name($id, $i)); + $res[$i]['type'] = @mysql_field_type($id, $i); + $res[$i]['len'] = @mysql_field_len($id, $i); + $res[$i]['flags'] = @mysql_field_flags($id, $i); + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @mysql_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SHOW TABLES'; + case 'views': + return DB_ERROR_NOT_CAPABLE; + case 'users': + $sql = 'select distinct User from user'; + if ($this->dsn['database'] != 'mysql') { + $dsn = $this->dsn; + $dsn['database'] = 'mysql'; + if (DB::isError($db = DB::connect($dsn))) { + return $db; + } + $sql = $db->getCol($sql); + $db->disconnect(); + // XXX Fixme the mysql driver should take care of this + if (!@mysql_select_db($this->dsn['database'], $this->connection)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + return $sql; + case 'databases': + return 'SHOW DATABASES'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/mysqli.php b/pear/DB/mysqli.php new file mode 100644 index 0000000..fa15b44 --- /dev/null +++ b/pear/DB/mysqli.php @@ -0,0 +1,960 @@ + | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// EXPERIMENTAL + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's mysqli + * extension. + * + * This is for MySQL versions 4.1 and above. Requires PHP 5. + * + * Note that persistent connections no longer exist. + * + * @package DB + * @version $Id$ + * @category Database + * @author Daniel Convissor + * @since Class functional since Release 1.6.3 + */ +class DB_mysqli extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $num_rows = array(); + var $transaction_opcount = 0; + var $autocommit = true; + var $fetchmode = DB_FETCHMODE_ORDERED; /* Default fetch mode */ + var $_db = false; + + /** + * Array for converting MYSQLI_*_FLAG constants to text values + * @var array + * @access public + * @since Property available since Release 1.6.5 + */ + var $mysqli_flags = array( + MYSQLI_NOT_NULL_FLAG => 'not_null', + MYSQLI_PRI_KEY_FLAG => 'primary_key', + MYSQLI_UNIQUE_KEY_FLAG => 'unique_key', + MYSQLI_MULTIPLE_KEY_FLAG => 'multiple_key', + MYSQLI_BLOB_FLAG => 'blob', + MYSQLI_UNSIGNED_FLAG => 'unsigned', + MYSQLI_ZEROFILL_FLAG => 'zerofill', + MYSQLI_AUTO_INCREMENT_FLAG => 'auto_increment', + MYSQLI_TIMESTAMP_FLAG => 'timestamp', + MYSQLI_SET_FLAG => 'set', + // MYSQLI_NUM_FLAG => 'numeric', // unnecessary + // MYSQLI_PART_KEY_FLAG => 'multiple_key', // duplicatvie + MYSQLI_GROUP_FLAG => 'group_by' + ); + + /** + * Array for converting MYSQLI_TYPE_* constants to text values + * @var array + * @access public + * @since Property available since Release 1.6.5 + */ + var $mysqli_types = array( + MYSQLI_TYPE_DECIMAL => 'decimal', + MYSQLI_TYPE_TINY => 'tinyint', + MYSQLI_TYPE_SHORT => 'int', + MYSQLI_TYPE_LONG => 'int', + MYSQLI_TYPE_FLOAT => 'float', + MYSQLI_TYPE_DOUBLE => 'double', + // MYSQLI_TYPE_NULL => 'DEFAULT NULL', // let flags handle it + MYSQLI_TYPE_TIMESTAMP => 'timestamp', + MYSQLI_TYPE_LONGLONG => 'bigint', + MYSQLI_TYPE_INT24 => 'mediumint', + MYSQLI_TYPE_DATE => 'date', + MYSQLI_TYPE_TIME => 'time', + MYSQLI_TYPE_DATETIME => 'datetime', + MYSQLI_TYPE_YEAR => 'year', + MYSQLI_TYPE_NEWDATE => 'date', + MYSQLI_TYPE_ENUM => 'enum', + MYSQLI_TYPE_SET => 'set', + MYSQLI_TYPE_TINY_BLOB => 'tinyblob', + MYSQLI_TYPE_MEDIUM_BLOB => 'mediumblob', + MYSQLI_TYPE_LONG_BLOB => 'longblob', + MYSQLI_TYPE_BLOB => 'blob', + MYSQLI_TYPE_VAR_STRING => 'varchar', + MYSQLI_TYPE_STRING => 'char', + MYSQLI_TYPE_GEOMETRY => 'geometry', + ); + + // }}} + // {{{ constructor + + /** + * DB_mysql constructor. + * + * @access public + */ + function DB_mysqli() + { + $this->DB_common(); + $this->phptype = 'mysqli'; + $this->dbsyntax = 'mysqli'; + $this->features = array( + 'prepare' => false, + 'ssl' => true, + 'transactions' => true, + 'limit' => 'alter' + ); + $this->errorcode_map = array( + 1004 => DB_ERROR_CANNOT_CREATE, + 1005 => DB_ERROR_CANNOT_CREATE, + 1006 => DB_ERROR_CANNOT_CREATE, + 1007 => DB_ERROR_ALREADY_EXISTS, + 1008 => DB_ERROR_CANNOT_DROP, + 1022 => DB_ERROR_ALREADY_EXISTS, + 1046 => DB_ERROR_NODBSELECTED, + 1048 => DB_ERROR_CONSTRAINT, + 1050 => DB_ERROR_ALREADY_EXISTS, + 1051 => DB_ERROR_NOSUCHTABLE, + 1054 => DB_ERROR_NOSUCHFIELD, + 1062 => DB_ERROR_ALREADY_EXISTS, + 1064 => DB_ERROR_SYNTAX, + 1100 => DB_ERROR_NOT_LOCKED, + 1136 => DB_ERROR_VALUE_COUNT_ON_ROW, + 1146 => DB_ERROR_NOSUCHTABLE, + 1216 => DB_ERROR_CONSTRAINT, + 1217 => DB_ERROR_CONSTRAINT, + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param string $dsn the data source name (see DB::parseDSN for syntax) + * @param boolean $persistent (optional) whether the connection should + * be persistent + * @return mixed DB_OK on success, a DB error on failure + * @access public + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('mysqli')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + $conn = false; + @ini_set('track_errors', true); + + if ($this->getOption('ssl') === true) { + $init = mysqli_init(); + mysqli_ssl_set( + $init, + empty($dsninfo['key']) ? null : $dsninfo['key'], + empty($dsninfo['cert']) ? null : $dsninfo['cert'], + empty($dsninfo['ca']) ? null : $dsninfo['ca'], + empty($dsninfo['capath']) ? null : $dsninfo['capath'], + empty($dsninfo['cipher']) ? null : $dsninfo['cipher'] + ); + if ($conn = @mysqli_real_connect($init, + $dsninfo['hostspec'], + $dsninfo['username'], + $dsninfo['password'], + $dsninfo['database'], + $dsninfo['port'], + $dsninfo['socket'])) + { + $conn = $init; + } + } else { + $conn = @mysqli_connect( + $dsninfo['hostspec'], + $dsninfo['username'], + $dsninfo['password'], + $dsninfo['database'], + $dsninfo['port'], + $dsninfo['socket'] + ); + } + + @ini_restore('track_errors'); + + if (!$conn) { + if (($err = @mysqli_connect_error()) != '') { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $err); + } elseif (empty($php_errormsg)) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED); + } else { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $php_errormsg); + } + } + + if ($dsninfo['database']) { + $this->_db = $dsninfo['database']; + } + + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @return boolean true on success, false if not connected + * @access public + */ + function disconnect() + { + $ret = @mysqli_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to MySQL and return the results as a MySQL resource + * identifier. + * + * @param string $query the SQL query + * @return mixed a valid MySQL result for successful SELECT + * queries, DB_OK for other successful queries. + * A DB error is returned on failure. + * @access public + */ + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=0'); + $result = @mysqli_query($this->connection, 'BEGIN'); + if (!$result) { + return $this->mysqlRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @mysqli_query($this->connection, $query); + if (!$result) { + return $this->mysqlRaiseError(); + } +# this next block is still sketchy.. + if (is_object($result)) { + $numrows = $this->numrows($result); + if (is_object($numrows)) { + return $numrows; + } +# need to come up with different means for next line +# since $result is object (int)$result won't fly... +// $this->num_rows[(int)$result] = $numrows; + return $result; + } + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal mysql result pointer to the next available result. + * + * This method has not been implemented yet. + * + * @param resource $result a valid sql result resource + * @return false + * @access public + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@mysqli_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @mysqli_fetch_array($result, MYSQLI_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @mysqli_fetch_row($result); + } + if (!$arr) { + $errno = @mysqli_errno($this->connection); + if (!$errno) { + return null; + } + return $this->mysqlRaiseError($errno); + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + /* + * Even though this DBMS already trims output, we do this because + * a field might have intentional whitespace at the end that + * gets removed by DB_PORTABILITY_RTRIM under another driver. + */ + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param resource $result MySQL result identifier + * @return bool true on success, false if $result is invalid + * @access public + */ + function freeResult($result) + { +# need to come up with different means for next line +# since $result is object (int)$result won't fly... +// unset($this->num_rows[(int)$result]); + return @mysqli_free_result($result); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result MySQL result identifier + * + * @access public + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + $cols = @mysqli_num_fields($result); + + if (!$cols) { + return $this->mysqlRaiseError(); + } + + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Get the number of rows in a result set. + * + * @param resource $result MySQL result identifier + * @return int the number of rows in $result + * @access public + */ + function numRows($result) + { + $rows = @mysqli_num_rows($result); + if ($rows === null) { + return $this->mysqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits. + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the current transaction. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysqli_query($this->connection, 'COMMIT'); + $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=1'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back (undo) the current transaction. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if ($this->_db) { + if (!@mysqli_select_db($this->connection, $this->_db)) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + $result = @mysqli_query($this->connection, 'ROLLBACK'); + $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=1'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->mysqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the data manipulation + * query. For other queries, this function returns 0. + * + * @return integer number of rows affected by the last query + */ + function affectedRows() + { + if (DB::isManip($this->last_query)) { + return @mysqli_affected_rows($this->connection); + } else { + return 0; + } + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error code of the last error (if any) that + * occured on the current connection. + * + * @return int native MySQL error code + * @access public + */ + function errorNative() + { + return @mysqli_errno($this->connection); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("UPDATE ${seqname} ". + 'SET id=LAST_INSERT_ID(id+1)'); + $this->popErrorHandling(); + if ($result === DB_OK) { + /** COMMON CASE **/ + $id = @mysqli_insert_id($this->connection); + if ($id != 0) { + return $id; + } + /** EMPTY SEQ TABLE **/ + // Sequence table must be empty for some reason, so fill it and return 1 + // Obtain a user-level lock + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + if ($result == 0) { + // Failed to get the lock, bail with a DB_ERROR_NOT_LOCKED error + return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED); + } + + // add the default value + $result = $this->query("REPLACE INTO ${seqname} (id) VALUES (0)"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + + // Release the lock + $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')"); + if (DB::isError($result)) { + return $this->raiseError($result); + } + // We know what the result will be, so no need to try again + return 1; + + /** ONDEMAND TABLE CREATION **/ + } elseif ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) + { + $result = $this->createSequence($seq_name); + // Since createSequence initializes the ID to be 1, + // we do not need to retrieve the ID again (or we will get 2) + if (DB::isError($result)) { + return $this->raiseError($result); + } else { + // First ID of a newly created sequence is 1 + return 1; + } + + /** BACKWARDS COMPAT **/ + } elseif (DB::isError($result) && + $result->getCode() == DB_ERROR_ALREADY_EXISTS) + { + // see _BCsequence() comment + $result = $this->_BCsequence($seqname); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $repeat = 1; + } + } while ($repeat); + + return $this->raiseError($result); + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $res = $this->query("CREATE TABLE ${seqname} ". + '(id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,'. + ' PRIMARY KEY(id))'); + if (DB::isError($res)) { + return $res; + } + // insert yields value 1, nextId call will generate ID 2 + return $this->query("INSERT INTO ${seqname} (id) VALUES (0)"); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)); + } + + // }}} + // {{{ _BCsequence() + + /** + * Backwards compatibility with old sequence emulation implementation + * (clean up the dupes). + * + * @param string $seqname The sequence name to clean up + * @return mixed DB_Error or true + */ + function _BCsequence($seqname) + { + // Obtain a user-level lock... this will release any previous + // application locks, but unlike LOCK TABLES, it does not abort + // the current transaction and is much less frequently used. + $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)"); + if (DB::isError($result)) { + return $result; + } + if ($result == 0) { + // Failed to get the lock, can't do the conversion, bail + // with a DB_ERROR_NOT_LOCKED error + return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED); + } + + $highest_id = $this->getOne("SELECT MAX(id) FROM ${seqname}"); + if (DB::isError($highest_id)) { + return $highest_id; + } + // This should kill all rows except the highest + // We should probably do something if $highest_id isn't + // numeric, but I'm at a loss as how to handle that... + $result = $this->query("DELETE FROM ${seqname} WHERE id <> $highest_id"); + if (DB::isError($result)) { + return $result; + } + + // If another thread has been waiting for this lock, + // it will go thru the above procedure, but will have no + // real effect + $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')"); + if (DB::isError($result)) { + return $result; + } + return true; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quote a string so it can be safely used as a table or column name + * + * Quoting style depends on which database driver is being used. + * + * MySQL can't handle the backtick character (`) in + * table or column names. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @since 1.6.0 + * @access public + * @internal + */ + function quoteIdentifier($str) + { + return '`' . $str . '`'; + } + + // }}} + // {{{ escapeSimple() + + /** + * Escape a string according to the current DBMS's standards + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @internal + */ + function escapeSimple($str) { + return @mysqli_real_escape_string($this->connection, $str); + } + + // }}} + // {{{ modifyQuery() + + function modifyQuery($query) + { + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + function modifyLimitQuery($query, $from, $count, $params = array()) + { + if (DB::isManip($query)) { + return $query . " LIMIT $count"; + } else { + return $query . " LIMIT $from, $count"; + } + } + + // }}} + // {{{ mysqlRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see DB_common::errorCode() + * @see DB_common::raiseError() + */ + function mysqlRaiseError($errno = null) + { + if ($errno === null) { + if ($this->options['portability'] & DB_PORTABILITY_ERRORS) { + $this->errorcode_map[1022] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT_NOT_NULL; + $this->errorcode_map[1062] = DB_ERROR_CONSTRAINT; + } else { + // Doing this in case mode changes during runtime. + $this->errorcode_map[1022] = DB_ERROR_ALREADY_EXISTS; + $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT; + $this->errorcode_map[1062] = DB_ERROR_ALREADY_EXISTS; + } + $errno = $this->errorCode(mysqli_errno($this->connection)); + } + return $this->raiseError($errno, null, null, null, + @mysqli_errno($this->connection) . ' ** ' . + @mysqli_error($this->connection)); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * WARNING: this method will probably not work because the mysqli_*() + * functions it relies upon may not exist. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @mysqli_query($this->connection, + "SELECT * FROM $result LIMIT 0"); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_a($id, 'mysqli_result')) { + return $this->mysqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @mysqli_num_fields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + for ($i=0; $i<$count; $i++) { + $tmp = @mysqli_fetch_field($id); + $res[$i]['table'] = $case_func($tmp->table); + $res[$i]['name'] = $case_func($tmp->name); + $res[$i]['type'] = isset($this->mysqli_types[$tmp->type]) ? + $this->mysqli_types[$tmp->type] : + 'unknown'; + $res[$i]['len'] = $tmp->max_length; + + $res[$i]['flags'] = ''; + foreach ($this->mysqli_flags as $const => $means) { + if ($tmp->flags & $const) { + $res[$i]['flags'] .= $means . ' '; + } + } + if ($tmp->def) { + $res[$i]['flags'] .= 'default_' . rawurlencode($tmp->def); + } + $res[$i]['flags'] = trim($res[$i]['flags']); + } + } else { // full + $res['num_fields']= $count; + + for ($i=0; $i<$count; $i++) { + $tmp = @mysqli_fetch_field($id); + $res[$i]['table'] = $case_func($tmp->table); + $res[$i]['name'] = $case_func($tmp->name); + $res[$i]['type'] = isset($this->mysqli_types[$tmp->type]) ? + $this->mysqli_types[$tmp->type] : + 'unknown'; + $res[$i]['len'] = $tmp->max_length; + + $res[$i]['flags'] = ''; + foreach ($this->mysqli_flags as $const => $means) { + if ($tmp->flags & $const) { + $res[$i]['flags'] .= $means . ' '; + } + } + if ($tmp->def) { + $res[$i]['flags'] .= 'default_' . rawurlencode($tmp->def); + } + $res[$i]['flags'] = trim($res[$i]['flags']); + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @mysqli_free_result($id); + } + return $res; + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info. + * + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SHOW TABLES'; + case 'views': + return DB_ERROR_NOT_CAPABLE; + case 'users': + $sql = 'select distinct User from user'; + if ($this->dsn['database'] != 'mysql') { + $dsn = $this->dsn; + $dsn['database'] = 'mysql'; + if (DB::isError($db = DB::connect($dsn))) { + return $db; + } + $sql = $db->getCol($sql); + $db->disconnect(); + // XXX Fixme the mysql driver should take care of this + if (!@mysqli_select_db($this->connection, $this->dsn['database'])) { + return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED); + } + } + return $sql; + case 'databases': + return 'SHOW DATABASES'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/oci8.php b/pear/DB/oci8.php new file mode 100644 index 0000000..42f48b0 --- /dev/null +++ b/pear/DB/oci8.php @@ -0,0 +1,899 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// be aware... OCIError() only appears to return anything when given a +// statement, so functions return the generic DB_ERROR instead of more +// useful errors that have to do with feedback from the database. + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's Oracle 8 + * call-interface extension. + * + * Definitely works with versions 8 and 9 of Oracle. + * + * @package DB + * @version $Id$ + * @category Database + * @author James L. Pine + */ +class DB_oci8 extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $manip_query = array(); + var $prepare_types = array(); + var $autoCommit = 1; + var $last_stmt = false; + + /** + * stores the $data passed to execute() in the oci8 driver + * + * Gets reset to array() when simpleQuery() is run. + * + * Needed in case user wants to call numRows() after prepare/execute + * was used. + * + * @var array + * @access private + */ + var $_data = array(); + + // }}} + // {{{ constructor + + function DB_oci8() + { + $this->DB_common(); + $this->phptype = 'oci8'; + $this->dbsyntax = 'oci8'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => true, + 'limit' => 'alter' + ); + $this->errorcode_map = array( + 1 => DB_ERROR_CONSTRAINT, + 900 => DB_ERROR_SYNTAX, + 904 => DB_ERROR_NOSUCHFIELD, + 921 => DB_ERROR_SYNTAX, + 923 => DB_ERROR_SYNTAX, + 942 => DB_ERROR_NOSUCHTABLE, + 955 => DB_ERROR_ALREADY_EXISTS, + 1400 => DB_ERROR_CONSTRAINT_NOT_NULL, + 1407 => DB_ERROR_CONSTRAINT_NOT_NULL, + 1476 => DB_ERROR_DIVZERO, + 1722 => DB_ERROR_INVALID_NUMBER, + 2289 => DB_ERROR_NOSUCHTABLE, + 2291 => DB_ERROR_CONSTRAINT, + 2292 => DB_ERROR_CONSTRAINT, + 2449 => DB_ERROR_CONSTRAINT, + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * + * @return int DB_OK on success, a DB error code on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('oci8')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + $this->dsn = $dsninfo; + + $connect_function = $persistent ? 'OCIPLogon' : 'OCILogon'; + + if ($dsninfo['hostspec']) { + $conn = @$connect_function($dsninfo['username'], + $dsninfo['password'], + $dsninfo['hostspec']); + } elseif ($dsninfo['username'] || $dsninfo['password']) { + $conn = @$connect_function($dsninfo['username'], + $dsninfo['password']); + } else { + $conn = false; + } + if ($conn == false) { + $error = OCIError(); + $error = (is_array($error)) ? $error['message'] : null; + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $error); + } + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @return bool true on success, false if not connected. + */ + function disconnect() + { + $ret = @OCILogOff($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to oracle and return the results as an oci8 resource + * identifier. + * + * @param $query the SQL query + * + * @return int returns a valid oci8 result for successful SELECT + * queries, DB_OK for other successful queries. A DB error code + * is returned on failure. + */ + function simpleQuery($query) + { + $this->_data = array(); + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @OCIParse($this->connection, $query); + if (!$result) { + return $this->oci8RaiseError(); + } + if ($this->autoCommit) { + $success = @OCIExecute($result,OCI_COMMIT_ON_SUCCESS); + } else { + $success = @OCIExecute($result,OCI_DEFAULT); + } + if (!$success) { + return $this->oci8RaiseError($result); + } + $this->last_stmt=$result; + // Determine which queries that should return data, and which + // should return an error code only. + return DB::isManip($query) ? DB_OK : $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal oracle result pointer to the next available result + * + * @param a valid oci8 result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $moredata = @OCIFetchInto($result,$arr,OCI_ASSOC+OCI_RETURN_NULLS+OCI_RETURN_LOBS); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && + $moredata) + { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $moredata = OCIFetchInto($result,$arr,OCI_RETURN_NULLS+OCI_RETURN_LOBS); + } + if (!$moredata) { + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result oci8 result identifier + * + * @return bool true on success, false if $result is invalid + */ + function freeResult($result) + { + return @OCIFreeStatement($result); + } + + /** + * Free the internal resources associated with a prepared query. + * + * @param $stmt oci8 statement identifier + * + * @return bool true on success, false if $result is invalid + */ + function freePrepared($stmt) + { + if (isset($this->prepare_types[(int)$stmt])) { + unset($this->prepare_types[(int)$stmt]); + unset($this->manip_query[(int)$stmt]); + } else { + return false; + } + return true; + } + + // }}} + // {{{ numRows() + + function numRows($result) + { + // emulate numRows for Oracle. yuck. + if ($this->options['portability'] & DB_PORTABILITY_NUMROWS && + $result === $this->last_stmt) + { + $countquery = 'SELECT COUNT(*) FROM ('.$this->last_query.')'; + $save_query = $this->last_query; + $save_stmt = $this->last_stmt; + + if (count($this->_data)) { + $smt = $this->prepare('SELECT COUNT(*) FROM ('.$this->last_query.')'); + $count = $this->execute($smt, $this->_data); + } else { + $count =& $this->query($countquery); + } + + if (DB::isError($count) || + DB::isError($row = $count->fetchRow(DB_FETCHMODE_ORDERED))) + { + $this->last_query = $save_query; + $this->last_stmt = $save_stmt; + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + return $row[0]; + } + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result oci8 result identifier + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + $cols = @OCINumCols($result); + if (!$cols) { + return $this->oci8RaiseError($result); + } + return $cols; + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error code of the last error (if any) that occured + * on the current connection. This does not work, as OCIError does + * not work unless given a statement. If OCIError does return + * something, so will this. + * + * @return int native oci8 error code + */ + function errorNative() + { + if (is_resource($this->last_stmt)) { + $error = @OCIError($this->last_stmt); + } else { + $error = @OCIError($this->connection); + } + if (is_array($error)) { + return $error['code']; + } + return false; + } + + // }}} + // {{{ prepare() + + /** + * Prepares a query for multiple execution with execute(). + * + * With oci8, this is emulated. + * + * prepare() requires a generic query as string like + * INSERT INTO numbers VALUES (?, ?, ?) + * . The ? characters are placeholders. + * + * Three types of placeholders can be used: + * + ? a quoted scalar value, i.e. strings, integers + * + ! value is inserted 'as is' + * + & requires a file name. The file's contents get + * inserted into the query (i.e. saving binary + * data in a db) + * + * Use backslashes to escape placeholder characters if you don't want + * them to be interpreted as placeholders. Example: + * "UPDATE foo SET col=? WHERE col='over \& under'" + * + * + * @param string $query query to be prepared + * @return mixed DB statement resource on success. DB_Error on failure. + */ + function prepare($query) + { + $tokens = preg_split('/((? $val) { + switch ($val) { + case '?': + $types[$token++] = DB_PARAM_SCALAR; + unset($tokens[$key]); + break; + case '&': + $types[$token++] = DB_PARAM_OPAQUE; + unset($tokens[$key]); + break; + case '!': + $types[$token++] = DB_PARAM_MISC; + unset($tokens[$key]); + break; + default: + $tokens[$key] = preg_replace('/\\\([&?!])/', "\\1", $val); + if ($key != $binds) { + $newquery .= $tokens[$key] . ':bind' . $token; + } else { + $newquery .= $tokens[$key]; + } + } + } + + $this->last_query = $query; + $newquery = $this->modifyQuery($newquery); + if (!$stmt = @OCIParse($this->connection, $newquery)) { + return $this->oci8RaiseError(); + } + $this->prepare_types[(int)$stmt] = $types; + $this->manip_query[(int)$stmt] = DB::isManip($query); + return $stmt; + } + + // }}} + // {{{ execute() + + /** + * Executes a DB statement prepared with prepare(). + * + * @param resource $stmt a DB statement resource returned from prepare() + * @param mixed $data array, string or numeric data to be used in + * execution of the statement. Quantity of items + * passed must match quantity of placeholders in + * query: meaning 1 for non-array items or the + * quantity of elements in the array. + * @return int returns an oci8 result resource for successful + * SELECT queries, DB_OK for other successful queries. A DB error + * code is returned on failure. + * @see DB_oci::prepare() + */ + function &execute($stmt, $data = array()) + { + if (!is_array($data)) { + $data = array($data); + } + + $this->_data = $data; + + $types =& $this->prepare_types[(int)$stmt]; + if (count($types) != count($data)) { + $tmp =& $this->raiseError(DB_ERROR_MISMATCH); + return $tmp; + } + + $i = 0; + foreach ($data as $key => $value) { + if ($types[$i] == DB_PARAM_MISC) { + /* + * Oracle doesn't seem to have the ability to pass a + * parameter along unchanged, so strip off quotes from start + * and end, plus turn two single quotes to one single quote, + * in order to avoid the quotes getting escaped by + * Oracle and ending up in the database. + */ + $data[$key] = preg_replace("/^'(.*)'$/", "\\1", $data[$key]); + $data[$key] = str_replace("''", "'", $data[$key]); + } elseif ($types[$i] == DB_PARAM_OPAQUE) { + $fp = @fopen($data[$key], 'rb'); + if (!$fp) { + $tmp =& $this->raiseError(DB_ERROR_ACCESS_VIOLATION); + return $tmp; + } + $data[$key] = fread($fp, filesize($data[$key])); + fclose($fp); + } + if (!@OCIBindByName($stmt, ':bind' . $i, $data[$key], -1)) { + $tmp = $this->oci8RaiseError($stmt); + return $tmp; + } + $i++; + } + if ($this->autoCommit) { + $success = @OCIExecute($stmt, OCI_COMMIT_ON_SUCCESS); + } else { + $success = @OCIExecute($stmt, OCI_DEFAULT); + } + if (!$success) { + $tmp = $this->oci8RaiseError($stmt); + return $tmp; + } + $this->last_stmt = $stmt; + if ($this->manip_query[(int)$stmt]) { + $tmp = DB_OK; + } else { + $tmp =& new DB_result($this, $stmt); + } + return $tmp; + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits + * + * @param $onoff true/false whether to autocommit + */ + function autoCommit($onoff = false) + { + $this->autoCommit = (bool)$onoff;; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit transactions on the current connection + * + * @return DB_ERROR or DB_OK + */ + function commit() + { + $result = @OCICommit($this->connection); + if (!$result) { + return $this->oci8RaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back all uncommitted transactions on the current connection. + * + * @return DB_ERROR or DB_OK + */ + function rollback() + { + $result = @OCIRollback($this->connection); + if (!$result) { + return $this->oci8RaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the last query. + * if the last query was a select, returns 0. + * + * @return number of rows affected by the last query or DB_ERROR + */ + function affectedRows() + { + if ($this->last_stmt === false) { + return $this->oci8RaiseError(); + } + $result = @OCIRowCount($this->last_stmt); + if ($result === false) { + return $this->oci8RaiseError($this->last_stmt); + } + return $result; + } + + // }}} + // {{{ modifyQuery() + + function modifyQuery($query) + { + // "SELECT 2+2" must be "SELECT 2+2 FROM dual" in Oracle + if (preg_match('/^\s*SELECT/i', $query) && + !preg_match('/\sFROM\s/i', $query)) { + $query .= ' FROM dual'; + } + return $query; + } + + // }}} + // {{{ modifyLimitQuery() + + /** + * Emulate the row limit support altering the query + * + * @param string $query The query to treat + * @param int $from The row to start to fetch from + * @param int $count The offset + * @return string The modified query + * + * @author Tomas V.V.Cox + */ + function modifyLimitQuery($query, $from, $count, $params = array()) + { + // Let Oracle return the name of the columns instead of + // coding a "home" SQL parser + + if (count($params)) { + $result = $this->prepare("SELECT * FROM ($query) " + . 'WHERE NULL = NULL'); + $tmp =& $this->execute($result, $params); + } else { + $q_fields = "SELECT * FROM ($query) WHERE NULL = NULL"; + + if (!$result = @OCIParse($this->connection, $q_fields)) { + $this->last_query = $q_fields; + return $this->oci8RaiseError(); + } + if (!@OCIExecute($result, OCI_DEFAULT)) { + $this->last_query = $q_fields; + return $this->oci8RaiseError($result); + } + } + + $ncols = OCINumCols($result); + $cols = array(); + for ( $i = 1; $i <= $ncols; $i++ ) { + $cols[] = '"' . OCIColumnName($result, $i) . '"'; + } + $fields = implode(', ', $cols); + // XXX Test that (tip by John Lim) + //if (preg_match('/^\s*SELECT\s+/is', $query, $match)) { + // // Introduce the FIRST_ROWS Oracle query optimizer + // $query = substr($query, strlen($match[0]), strlen($query)); + // $query = "SELECT /* +FIRST_ROWS */ " . $query; + //} + + // Construct the query + // more at: http://marc.theaimsgroup.com/?l=php-db&m=99831958101212&w=2 + // Perhaps this could be optimized with the use of Unions + $query = "SELECT $fields FROM". + " (SELECT rownum as linenum, $fields FROM". + " ($query)". + ' WHERE rownum <= '. ($from + $count) . + ') WHERE linenum >= ' . ++$from; + return $query; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = 0; + do { + $this->expectError(DB_ERROR_NOSUCHTABLE); + $result =& $this->query("SELECT ${seqname}.nextval FROM dual"); + $this->popExpect(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } else { + $repeat = 0; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + return $arr[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("CREATE SEQUENCE ${seqname}"); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP SEQUENCE ${seqname}"); + } + + // }}} + // {{{ oci8RaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see DB_common::errorCode() + * @see DB_common::raiseError() + */ + function oci8RaiseError($errno = null) + { + if ($errno === null) { + $error = @OCIError($this->connection); + return $this->raiseError($this->errorCode($error['code']), + null, null, null, $error['message']); + } elseif (is_resource($errno)) { + $error = @OCIError($errno); + return $this->raiseError($this->errorCode($error['code']), + null, null, null, $error['message']); + } + return $this->raiseError($this->errorCode($errno)); + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return 'SELECT table_name FROM user_tables'; + default: + return null; + } + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * NOTE: only supports 'table' and 'flags' if $result + * is a table name. + * + * NOTE: flags won't contain index information. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + if (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $result = strtoupper($result); + $q_fields = 'SELECT column_name, data_type, data_length, ' + . 'nullable ' + . 'FROM user_tab_columns ' + . "WHERE table_name='$result' ORDER BY column_id"; + + $this->last_query = $q_fields; + + if (!$stmt = @OCIParse($this->connection, $q_fields)) { + return $this->oci8RaiseError(DB_ERROR_NEED_MORE_DATA); + } + if (!@OCIExecute($stmt, OCI_DEFAULT)) { + return $this->oci8RaiseError($stmt); + } + + $i = 0; + while (@OCIFetch($stmt)) { + $res[$i]['table'] = $case_func($result); + $res[$i]['name'] = $case_func(@OCIResult($stmt, 1)); + $res[$i]['type'] = @OCIResult($stmt, 2); + $res[$i]['len'] = @OCIResult($stmt, 3); + $res[$i]['flags'] = (@OCIResult($stmt, 4) == 'N') ? 'not_null' : ''; + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + $i++; + } + + if ($mode) { + $res['num_fields'] = $i; + } + @OCIFreeStatement($stmt); + + } else { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $result = $result->result; + } else { + /* + * ELSE, probably received a result resource identifier. + * Deprecated. Here for compatibility only. + */ + } + + if ($result === $this->last_stmt) { + $count = @OCINumCols($result); + + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = ''; + $res[$i]['name'] = $case_func(@OCIColumnName($result, $i+1)); + $res[$i]['type'] = @OCIColumnType($result, $i+1); + $res[$i]['len'] = @OCIColumnSize($result, $i+1); + $res[$i]['flags'] = ''; + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + + if ($mode) { + $res['num_fields'] = $count; + } + + } else { + return $this->raiseError(DB_ERROR_NOT_CAPABLE); + } + } + return $res; + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/odbc.php b/pear/DB/odbc.php new file mode 100644 index 0000000..d0f50a1 --- /dev/null +++ b/pear/DB/odbc.php @@ -0,0 +1,585 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// XXX legend: +// More info on ODBC errors could be found here: +// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/trblsql/tr_err_odbc_5stz.asp +// +// XXX ERRORMSG: The error message from the odbc function should +// be registered here. + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's ODBC + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Stig Bakken + */ +class DB_odbc extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $row = array(); + + // }}} + // {{{ constructor + + function DB_odbc() + { + $this->DB_common(); + $this->phptype = 'odbc'; + $this->dbsyntax = 'sql92'; + $this->features = array( + 'prepare' => true, + 'pconnect' => true, + 'transactions' => false, + 'limit' => 'emulate' + ); + $this->errorcode_map = array( + '01004' => DB_ERROR_TRUNCATED, + '07001' => DB_ERROR_MISMATCH, + '21S01' => DB_ERROR_MISMATCH, + '21S02' => DB_ERROR_MISMATCH, + '22003' => DB_ERROR_INVALID_NUMBER, + '22005' => DB_ERROR_INVALID_NUMBER, + '22008' => DB_ERROR_INVALID_DATE, + '22012' => DB_ERROR_DIVZERO, + '23000' => DB_ERROR_CONSTRAINT, + '23502' => DB_ERROR_CONSTRAINT_NOT_NULL, + '23503' => DB_ERROR_CONSTRAINT, + '23505' => DB_ERROR_CONSTRAINT, + '24000' => DB_ERROR_INVALID, + '34000' => DB_ERROR_INVALID, + '37000' => DB_ERROR_SYNTAX, + '42000' => DB_ERROR_SYNTAX, + '42601' => DB_ERROR_SYNTAX, + 'IM001' => DB_ERROR_UNSUPPORTED, + 'S0000' => DB_ERROR_NOSUCHTABLE, + 'S0001' => DB_ERROR_ALREADY_EXISTS, + 'S0002' => DB_ERROR_NOSUCHTABLE, + 'S0011' => DB_ERROR_ALREADY_EXISTS, + 'S0012' => DB_ERROR_NOT_FOUND, + 'S0021' => DB_ERROR_ALREADY_EXISTS, + 'S0022' => DB_ERROR_NOSUCHFIELD, + 'S1000' => DB_ERROR_CONSTRAINT_NOT_NULL, + 'S1009' => DB_ERROR_INVALID, + 'S1090' => DB_ERROR_INVALID, + 'S1C00' => DB_ERROR_NOT_CAPABLE + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * + * @return int DB_OK on success, a DB error code on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('odbc')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + if ($dsninfo['dbsyntax']) { + $this->dbsyntax = $dsninfo['dbsyntax']; + } + switch ($this->dbsyntax) { + case 'solid': + $this->features = array( + 'prepare' => true, + 'pconnect' => true, + 'transactions' => true + ); + break; + case 'navision': + // the Navision driver doesn't support fetch row by number + $this->features['limit'] = false; + } + + /* + * This is hear for backwards compatibility. + * Should have been using 'database' all along, but used hostspec. + */ + if ($dsninfo['database']) { + $odbcdsn = $dsninfo['database']; + } elseif ($dsninfo['hostspec']) { + $odbcdsn = $dsninfo['hostspec']; + } else { + $odbcdsn = 'localhost'; + } + + if ($this->provides('pconnect')) { + $connect_function = $persistent ? 'odbc_pconnect' : 'odbc_connect'; + } else { + $connect_function = 'odbc_connect'; + } + + if (empty($dsninfo['cursor'])) { + $conn = @$connect_function($odbcdsn, $dsninfo['username'], + $dsninfo['password']); + } else { + $conn = @$connect_function($odbcdsn, $dsninfo['username'], + $dsninfo['password'], + $dsninfo['cursor']); + } + + if (!is_resource($conn)) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, null, + null, $this->errorNative()); + } + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + function disconnect() + { + $err = @odbc_close($this->connection); + $this->connection = null; + return $err; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to ODBC and return the results as a ODBC resource + * identifier. + * + * @param $query the SQL query + * + * @return int returns a valid ODBC result for successful SELECT + * queries, DB_OK for other successful queries. A DB error code + * is returned on failure. + */ + function simpleQuery($query) + { + $this->last_query = $query; + $query = $this->modifyQuery($query); + $result = @odbc_exec($this->connection, $query); + if (!$result) { + return $this->odbcRaiseError(); // XXX ERRORMSG + } + // Determine which queries that should return data, and which + // should return an error code only. + if (DB::isManip($query)) { + $this->manip_result = $result; // For affectedRows() + return DB_OK; + } + $this->row[(int)$result] = 0; + $this->manip_result = 0; + return $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal odbc result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return @odbc_next_result($result); + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + $arr = array(); + if ($rownum !== null) { + $rownum++; // ODBC first row is 1 + if (version_compare(phpversion(), '4.2.0', 'ge')) { + $cols = @odbc_fetch_into($result, $arr, $rownum); + } else { + $cols = @odbc_fetch_into($result, $rownum, $arr); + } + } else { + $cols = @odbc_fetch_into($result, $arr); + } + + if (!$cols) { + /* XXX FIXME: doesn't work with unixODBC and easysoft + (get corrupted $errno values) + if ($errno = @odbc_error($this->connection)) { + return $this->RaiseError($errno); + }*/ + return null; + } + if ($fetchmode !== DB_FETCHMODE_ORDERED) { + for ($i = 0; $i < count($arr); $i++) { + $colName = @odbc_field_name($result, $i+1); + $a[$colName] = $arr[$i]; + } + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $a = array_change_key_case($a, CASE_LOWER); + } + $arr = $a; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + function freeResult($result) + { + unset($this->row[(int)$result]); + return @odbc_free_result($result); + } + + // }}} + // {{{ numCols() + + function numCols($result) + { + $cols = @odbc_num_fields($result); + if (!$cols) { + return $this->odbcRaiseError(); + } + return $cols; + } + + // }}} + // {{{ affectedRows() + + /** + * Returns the number of rows affected by a manipulative query + * (INSERT, DELETE, UPDATE) + * @return mixed int affected rows, 0 when non manip queries or + * DB error on error + */ + function affectedRows() + { + if (empty($this->manip_result)) { // In case of SELECT stms + return 0; + } + $nrows = @odbc_num_rows($this->manip_result); + if ($nrows == -1) { + return $this->odbcRaiseError(); + } + return $nrows; + } + + // }}} + // {{{ numRows() + + /** + * ODBC may or may not support counting rows in the result set of + * SELECTs. + * + * @param $result the odbc result resource + * @return the number of rows, or 0 + */ + function numRows($result) + { + $nrows = @odbc_num_rows($result); + if ($nrows == -1) { + return $this->odbcRaiseError(DB_ERROR_UNSUPPORTED); + } + return $nrows; + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quote a string so it can be safely used as a table / column name + * + * Quoting style depends on which dbsyntax was passed in the DSN. + * + * Use 'mssql' as the dbsyntax in the DB DSN only if you've unchecked + * "Use ANSI quoted identifiers" when setting up the ODBC data source. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @since 1.6.0 + * @access public + */ + function quoteIdentifier($str) + { + switch ($this->dsn['dbsyntax']) { + case 'access': + return '[' . $str . ']'; + case 'mssql': + case 'sybase': + return '[' . str_replace(']', ']]', $str) . ']'; + case 'mysql': + case 'mysqli': + return '`' . $str . '`'; + default: + return '"' . str_replace('"', '""', $str) . '"'; + } + } + + // }}} + // {{{ quote() + + /** + * @deprecated Deprecated in release 1.6.0 + * @internal + */ + function quote($str) { + return $this->quoteSmart($str); + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error code of the last error (if any) that + * occured on the current connection. + * + * @access public + * + * @return int ODBC error code + */ + function errorNative() + { + if (!isset($this->connection) || !is_resource($this->connection)) { + return @odbc_error() . ' ' . @odbc_errormsg(); + } + return @odbc_error($this->connection) . ' ' . @odbc_errormsg($this->connection); + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("update ${seqname} set id = id + 1"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = 1; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->createSequence($seq_name); + $this->popErrorHandling(); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $result = $this->query("insert into ${seqname} (id) values(0)"); + } else { + $repeat = 0; + } + } while ($repeat); + + if (DB::isError($result)) { + return $this->raiseError($result); + } + + $result = $this->query("select id from ${seqname}"); + if (DB::isError($result)) { + return $result; + } + + $row = $result->fetchRow(DB_FETCHMODE_ORDERED); + if (DB::isError($row || !$row)) { + return $row; + } + + return $row[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("CREATE TABLE ${seqname} ". + '(id integer NOT NULL,'. + ' PRIMARY KEY(id))'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP TABLE ${seqname}"); + } + + // }}} + // {{{ autoCommit() + + function autoCommit($onoff = false) + { + if (!@odbc_autocommit($this->connection, $onoff)) { + return $this->odbcRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ commit() + + function commit() + { + if (!@odbc_commit($this->connection)) { + return $this->odbcRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + function rollback() + { + if (!@odbc_rollback($this->connection)) { + return $this->odbcRaiseError(); + } + return DB_OK; + } + + // }}} + // {{{ odbcRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see errorNative() + * @see DB_common::errorCode() + * @see DB_common::raiseError() + */ + function odbcRaiseError($errno = null) + { + if ($errno === null) { + switch ($this->dbsyntax) { + case 'access': + if ($this->options['portability'] & DB_PORTABILITY_ERRORS) { + $this->errorcode_map['07001'] = DB_ERROR_NOSUCHFIELD; + } else { + // Doing this in case mode changes during runtime. + $this->errorcode_map['07001'] = DB_ERROR_MISMATCH; + } + } + $errno = $this->errorCode(odbc_error($this->connection)); + } + return $this->raiseError($errno, null, null, null, + $this->errorNative()); + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/pgsql.php b/pear/DB/pgsql.php new file mode 100644 index 0000000..2b011c9 --- /dev/null +++ b/pear/DB/pgsql.php @@ -0,0 +1,847 @@ + | +// | Stig Bakken | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's PostgreSQL + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Rui Hirokawa + * @author Stig Bakken + */ +class DB_pgsql extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $transaction_opcount = 0; + var $dsn = array(); + var $row = array(); + var $num_rows = array(); + var $affected = 0; + var $autocommit = true; + var $fetchmode = DB_FETCHMODE_ORDERED; + + // }}} + // {{{ constructor + + function DB_pgsql() + { + $this->DB_common(); + $this->phptype = 'pgsql'; + $this->dbsyntax = 'pgsql'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => true, + 'limit' => 'alter' + ); + $this->errorcode_map = array( + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * + * @return int DB_OK on success, a DB error code on failure. + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('pgsql')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + $protocol = $dsninfo['protocol'] ? $dsninfo['protocol'] : 'tcp'; + $connstr = ''; + + if ($protocol == 'tcp') { + if ($dsninfo['hostspec']) { + $connstr .= 'host=' . $dsninfo['hostspec']; + } + if ($dsninfo['port']) { + $connstr .= ' port=' . $dsninfo['port']; + } + } elseif ($protocol == 'unix') { + // Allow for pg socket in non-standard locations. + if ($dsninfo['socket']) { + $connstr .= 'host=' . $dsninfo['socket']; + } + if ($dsninfo['port']) { + $connstr .= ' port=' . $dsninfo['port']; + } + } + + if ($dsninfo['database']) { + $connstr .= ' dbname=\'' . addslashes($dsninfo['database']) . '\''; + } + if ($dsninfo['username']) { + $connstr .= ' user=\'' . addslashes($dsninfo['username']) . '\''; + } + if ($dsninfo['password']) { + $connstr .= ' password=\'' . addslashes($dsninfo['password']) . '\''; + } + if (!empty($dsninfo['options'])) { + $connstr .= ' options=' . $dsninfo['options']; + } + if (!empty($dsninfo['tty'])) { + $connstr .= ' tty=' . $dsninfo['tty']; + } + + $connect_function = $persistent ? 'pg_pconnect' : 'pg_connect'; + + $ini = ini_get('track_errors'); + if ($ini) { + $conn = @$connect_function($connstr); + } else { + ini_set('track_errors', 1); + $conn = @$connect_function($connstr); + ini_set('track_errors', $ini); + } + if ($conn == false) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED, null, + null, null, strip_tags($php_errormsg)); + } + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @return bool true on success, false if not connected. + */ + function disconnect() + { + $ret = @pg_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to PostgreSQL and return the results as a + * PostgreSQL resource identifier. + * + * @param $query the SQL query + * + * @return int returns a valid PostgreSQL result for successful SELECT + * queries, DB_OK for other successful queries. A DB error code + * is returned on failure. + */ + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + $query = $this->modifyQuery($query); + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @pg_exec($this->connection, 'begin;'); + if (!$result) { + return $this->pgsqlRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @pg_exec($this->connection, $query); + if (!$result) { + return $this->pgsqlRaiseError(); + } + // Determine which queries that should return data, and which + // should return an error code only. + if ($ismanip) { + $this->affected = @pg_cmdtuples($result); + return DB_OK; + } elseif (preg_match('/^\s*\(?\s*(SELECT(?!\s+INTO)|EXPLAIN|SHOW)\s/si', $query)) { + /* PostgreSQL commands: + ABORT, ALTER, BEGIN, CLOSE, CLUSTER, COMMIT, COPY, + CREATE, DECLARE, DELETE, DROP TABLE, EXPLAIN, FETCH, + GRANT, INSERT, LISTEN, LOAD, LOCK, MOVE, NOTIFY, RESET, + REVOKE, ROLLBACK, SELECT, SELECT INTO, SET, SHOW, + UNLISTEN, UPDATE, VACUUM + */ + $this->row[(int)$result] = 0; // reset the row counter. + $numrows = $this->numrows($result); + if (is_object($numrows)) { + return $numrows; + } + $this->num_rows[(int)$result] = $numrows; + $this->affected = 0; + return $result; + } else { + $this->affected = 0; + return DB_OK; + } + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal pgsql result pointer to the next available result + * + * @param a valid fbsql result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ errorCode() + + /** + * Determine PEAR::DB error code from the database's text error message. + * + * @param string $errormsg error message returned from the database + * @return integer an error number from a DB error constant + */ + function errorCode($errormsg) + { + static $error_regexps; + if (!isset($error_regexps)) { + $error_regexps = array( + '/(([Rr]elation|[Ss]equence|[Tt]able)( [\"\'].*[\"\'])? does not exist|[Cc]lass ".+" not found)$/' => DB_ERROR_NOSUCHTABLE, + '/[Cc]olumn [\"\'].*[\"\'] .*does not exist/' => DB_ERROR_NOSUCHFIELD, + '/[Rr]elation [\"\'].*[\"\'] already exists|[Cc]annot insert a duplicate key into (a )?unique index.*/' => DB_ERROR_ALREADY_EXISTS, + '/(divide|division) by zero$/' => DB_ERROR_DIVZERO, + '/pg_atoi: error in .*: can\'t parse /' => DB_ERROR_INVALID_NUMBER, + '/invalid input syntax for integer/' => DB_ERROR_INVALID_NUMBER, + '/ttribute [\"\'].*[\"\'] not found$|[Rr]elation [\"\'].*[\"\'] does not have attribute [\"\'].*[\"\']/' => DB_ERROR_NOSUCHFIELD, + '/parser: parse error at or near \"/' => DB_ERROR_SYNTAX, + '/syntax error at/' => DB_ERROR_SYNTAX, + '/permission denied/' => DB_ERROR_ACCESS_VIOLATION, + '/violates not-null constraint/' => DB_ERROR_CONSTRAINT_NOT_NULL, + '/violates [\w ]+ constraint/' => DB_ERROR_CONSTRAINT, + '/referential integrity violation/' => DB_ERROR_CONSTRAINT + ); + } + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + $result_int = (int)$result; + $rownum = ($rownum !== null) ? $rownum : $this->row[$result_int]; + if ($rownum >= $this->num_rows[$result_int]) { + return null; + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @pg_fetch_array($result, $rownum, PGSQL_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @pg_fetch_row($result, $rownum); + } + if (!$arr) { + $err = pg_errormessage($this->connection); + if (!$err) { + return null; + } + return $this->pgsqlRaiseError(); + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + $this->row[$result_int] = ++$rownum; + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result int PostgreSQL result identifier + * + * @return bool true on success, false if $result is invalid + */ + function freeResult($result) + { + if (is_resource($result)) { + unset($this->row[(int)$result]); + unset($this->num_rows[(int)$result]); + $this->affected = 0; + return @pg_freeresult($result); + } + return false; + } + + // }}} + // {{{ quote() + + /** + * @deprecated Deprecated in release 1.6.0 + * @internal + */ + function quote($str) { + return $this->quoteSmart($str); + } + + // }}} + // {{{ quoteSmart() + + /** + * Format input so it can be safely used in a query + * + * @param mixed $in data to be quoted + * + * @return mixed Submitted variable's type = returned value: + * + null = the string NULL + * + boolean = string TRUE or FALSE + * + integer or double = the unquoted number + * + other (including strings and numeric strings) = + * the data escaped according to MySQL's settings + * then encapsulated between single quotes + * + * @internal + */ + function quoteSmart($in) + { + if (is_int($in) || is_double($in)) { + return $in; + } elseif (is_bool($in)) { + return $in ? 'TRUE' : 'FALSE'; + } elseif (is_null($in)) { + return 'NULL'; + } else { + return "'" . $this->escapeSimple($in) . "'"; + } + } + + // }}} + // {{{ escapeSimple() + + /** + * Escape a string according to the current DBMS's standards + * + * PostgreSQL treats a backslash as an escape character, so they are + * removed. + * + * Not using pg_escape_string() yet because it requires PostgreSQL + * to be at version 7.2 or greater. + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @internal + */ + function escapeSimple($str) { + return str_replace("'", "''", str_replace('\\', '\\\\', $str)); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result resource PostgreSQL result identifier + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + $cols = @pg_numfields($result); + if (!$cols) { + return $this->pgsqlRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Get the number of rows in a result set. + * + * @param $result resource PostgreSQL result identifier + * + * @return int the number of rows in $result + */ + function numRows($result) + { + $rows = @pg_numrows($result); + if ($rows === null) { + return $this->pgsqlRaiseError(); + } + return $rows; + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error code of the last error (if any) that + * occured on the current connection. + * + * @return int native PostgreSQL error code + */ + function errorNative() + { + return pg_errormessage($this->connection); + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the current transaction. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + // (disabled) hack to shut up error messages from libpq.a + //@fclose(@fopen("php://stderr", "w")); + $result = @pg_exec($this->connection, 'end;'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->pgsqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back (undo) the current transaction. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + $result = @pg_exec($this->connection, 'abort;'); + $this->transaction_opcount = 0; + if (!$result) { + return $this->pgsqlRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the last query. + * if the last query was a select, returns 0. + * + * @return int number of rows affected by the last query or DB_ERROR + */ + function affectedRows() + { + return $this->affected; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + $repeat = false; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result =& $this->query("SELECT NEXTVAL('${seqname}')"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) { + $repeat = true; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->createSequence($seq_name); + $this->popErrorHandling(); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $arr = $result->fetchRow(DB_FETCHMODE_ORDERED); + $result->free(); + return $arr[0]; + } + + // }}} + // {{{ createSequence() + + /** + * Create the sequence + * + * @param string $seq_name the name of the sequence + * @return mixed DB_OK on success or DB error on error + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $result = $this->query("CREATE SEQUENCE ${seqname}"); + return $result; + } + + // }}} + // {{{ dropSequence() + + /** + * Drop a sequence + * + * @param string $seq_name the name of the sequence + * @return mixed DB_OK on success or DB error on error + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP SEQUENCE ${seqname}"); + } + + // }}} + // {{{ modifyLimitQuery() + + function modifyLimitQuery($query, $from, $count, $params = array()) + { + $query = $query . " LIMIT $count OFFSET $from"; + return $query; + } + + // }}} + // {{{ pgsqlRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see errorNative() + * @see errorCode() + * @see DB_common::raiseError() + */ + function pgsqlRaiseError($errno = null) + { + $native = $this->errorNative(); + if ($errno === null) { + $err = $this->errorCode($native); + } else { + $err = $errno; + } + return $this->raiseError($err, null, null, null, $native); + } + + // }}} + // {{{ _pgFieldFlags() + + /** + * Flags of a Field + * + * @param int $resource PostgreSQL result identifier + * @param int $num_field the field number + * + * @return string The flags of the field ("not_null", "default_value", + * "primary_key", "unique_key" and "multiple_key" + * are supported). The default value is passed + * through rawurlencode() in case there are spaces in it. + * @access private + */ + function _pgFieldFlags($resource, $num_field, $table_name) + { + $field_name = @pg_fieldname($resource, $num_field); + + $result = @pg_exec($this->connection, "SELECT f.attnotnull, f.atthasdef + FROM pg_attribute f, pg_class tab, pg_type typ + WHERE tab.relname = typ.typname + AND typ.typrelid = f.attrelid + AND f.attname = '$field_name' + AND tab.relname = '$table_name'"); + if (@pg_numrows($result) > 0) { + $row = @pg_fetch_row($result, 0); + $flags = ($row[0] == 't') ? 'not_null ' : ''; + + if ($row[1] == 't') { + $result = @pg_exec($this->connection, "SELECT a.adsrc + FROM pg_attribute f, pg_class tab, pg_type typ, pg_attrdef a + WHERE tab.relname = typ.typname AND typ.typrelid = f.attrelid + AND f.attrelid = a.adrelid AND f.attname = '$field_name' + AND tab.relname = '$table_name' AND f.attnum = a.adnum"); + $row = @pg_fetch_row($result, 0); + $num = preg_replace("/'(.*)'::\w+/", "\\1", $row[0]); + $flags .= 'default_' . rawurlencode($num) . ' '; + } + } else { + $flags = ''; + } + $result = @pg_exec($this->connection, "SELECT i.indisunique, i.indisprimary, i.indkey + FROM pg_attribute f, pg_class tab, pg_type typ, pg_index i + WHERE tab.relname = typ.typname + AND typ.typrelid = f.attrelid + AND f.attrelid = i.indrelid + AND f.attname = '$field_name' + AND tab.relname = '$table_name'"); + $count = @pg_numrows($result); + + for ($i = 0; $i < $count ; $i++) { + $row = @pg_fetch_row($result, $i); + $keys = explode(' ', $row[2]); + + if (in_array($num_field + 1, $keys)) { + $flags .= ($row[0] == 't' && $row[1] == 'f') ? 'unique_key ' : ''; + $flags .= ($row[1] == 't') ? 'primary_key ' : ''; + if (count($keys) > 1) + $flags .= 'multiple_key '; + } + } + + return trim($flags); + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * NOTE: only supports 'table' and 'flags' if $result + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + $id = @pg_exec($this->connection, "SELECT * FROM $result LIMIT 0"); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->pgsqlRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @pg_numfields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func(@pg_fieldname($id, $i)); + $res[$i]['type'] = @pg_fieldtype($id, $i); + $res[$i]['len'] = @pg_fieldsize($id, $i); + $res[$i]['flags'] = $got_string ? $this->_pgFieldflags($id, $i, $result) : ''; + } + + } else { // full + $res['num_fields']= $count; + + for ($i=0; $i<$count; $i++) { + $res[$i]['table'] = $got_string ? $case_func($result) : ''; + $res[$i]['name'] = $case_func(@pg_fieldname($id, $i)); + $res[$i]['type'] = @pg_fieldtype($id, $i); + $res[$i]['len'] = @pg_fieldsize($id, $i); + $res[$i]['flags'] = $got_string ? $this->_pgFieldFlags($id, $i, $result) : ''; + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @pg_freeresult($id); + } + return $res; + } + + // }}} + // {{{ getTablesQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return "SELECT c.relname as \"Name\" + FROM pg_class c, pg_user u + WHERE c.relowner = u.usesysid AND c.relkind = 'r' + AND not exists (select 1 from pg_views where viewname = c.relname) + AND c.relname !~ '^(pg_|sql_)' + UNION + SELECT c.relname as \"Name\" + FROM pg_class c + WHERE c.relkind = 'r' + AND not exists (select 1 from pg_views where viewname = c.relname) + AND not exists (select 1 from pg_user where usesysid = c.relowner) + AND c.relname !~ '^pg_'"; + case 'views': + // Table cols: viewname | viewowner | definition + return 'SELECT viewname FROM pg_views'; + case 'users': + // cols: usename |usesysid|usecreatedb|usetrace|usesuper|usecatupd|passwd |valuntil + return 'SELECT usename FROM pg_user'; + case 'databases': + return 'SELECT datname FROM pg_database'; + case 'functions': + return 'SELECT proname FROM pg_proc'; + default: + return null; + } + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/sqlite.php b/pear/DB/sqlite.php new file mode 100644 index 0000000..ef5535b --- /dev/null +++ b/pear/DB/sqlite.php @@ -0,0 +1,695 @@ + | +// | Mika Tuupola | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for the SQLite + * PECL extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Urs Gehrig + * @author Mika Tuupola + */ +class DB_sqlite extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $_lasterror = ''; + + // }}} + // {{{ constructor + + /** + * Constructor for this class. + * + * Error codes according to sqlite_exec. Error Codes specification is + * in the {@link http://sqlite.org/c_interface.html online manual}. + * + * This errorhandling based on sqlite_exec is not yet implemented. + * + * @access public + */ + function DB_sqlite() + { + + $this->DB_common(); + $this->phptype = 'sqlite'; + $this->dbsyntax = 'sqlite'; + $this->features = array ( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => false, + 'limit' => 'alter' + ); + + // SQLite data types, http://www.sqlite.org/datatypes.html + $this->keywords = array ( + 'BLOB' => '', + 'BOOLEAN' => '', + 'CHARACTER' => '', + 'CLOB' => '', + 'FLOAT' => '', + 'INTEGER' => '', + 'KEY' => '', + 'NATIONAL' => '', + 'NUMERIC' => '', + 'NVARCHAR' => '', + 'PRIMARY' => '', + 'TEXT' => '', + 'TIMESTAMP' => '', + 'UNIQUE' => '', + 'VARCHAR' => '', + 'VARYING' => '' + ); + $this->errorcode_map = array( + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database represented by a file. + * + * @param $dsn the data source name; the file is taken as + * database; "sqlite://root:@host/test.db?mode=0644" + * @param $persistent (optional) whether the connection should + * be persistent + * @access public + * @return int DB_OK on success, a DB error on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('sqlite')) { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + + if ($dsninfo['database']) { + if (!file_exists($dsninfo['database'])) { + if (!touch($dsninfo['database'])) { + return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND); + } + if (!isset($dsninfo['mode']) || + !is_numeric($dsninfo['mode'])) + { + $mode = 0644; + } else { + $mode = octdec($dsninfo['mode']); + } + if (!chmod($dsninfo['database'], $mode)) { + return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND); + } + if (!file_exists($dsninfo['database'])) { + return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND); + } + } + if (!is_file($dsninfo['database'])) { + return $this->sqliteRaiseError(DB_ERROR_INVALID); + } + if (!is_readable($dsninfo['database'])) { + return $this->sqliteRaiseError(DB_ERROR_ACCESS_VIOLATION); + } + } else { + return $this->sqliteRaiseError(DB_ERROR_ACCESS_VIOLATION); + } + + $connect_function = $persistent ? 'sqlite_popen' : 'sqlite_open'; + if (!($conn = @$connect_function($dsninfo['database']))) { + return $this->sqliteRaiseError(DB_ERROR_NODBSELECTED); + } + $this->connection = $conn; + + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @access public + * @return bool true on success, false if not connected. + * @todo fix return values + */ + function disconnect() + { + $ret = @sqlite_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to SQLite and returns the results as a SQLite resource + * identifier. + * + * @param the SQL query + * @access public + * @return mixed returns a valid SQLite result for successful SELECT + * queries, DB_OK for other successful queries. A DB error is + * returned on failure. + */ + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + $query = $this->_modifyQuery($query); + ini_set('track_errors', true); + $result = @sqlite_query($query, $this->connection); + ini_restore('track_errors'); + $this->_lasterror = isset($php_errormsg) ? $php_errormsg : ''; + $this->result = $result; + if (!$this->result) { + return $this->sqliteRaiseError(null); + } + + /* sqlite_query() seems to allways return a resource */ + /* so cant use that. Using $ismanip instead */ + if (!$ismanip) { + $numRows = $this->numRows($result); + + /* if numRows() returned PEAR_Error */ + if (is_object($numRows)) { + return $numRows; + } + return $result; + } + return DB_OK; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal sqlite result pointer to the next available result. + * + * @param a valid sqlite result resource + * @access public + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@sqlite_seek($this->result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + $arr = @sqlite_fetch_array($result, SQLITE_ASSOC); + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @sqlite_fetch_array($result, SQLITE_NUM); + } + if (!$arr) { + /* See: http://bugs.php.net/bug.php?id=22328 */ + return null; + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + /* + * Even though this DBMS already trims output, we do this because + * a field might have intentional whitespace at the end that + * gets removed by DB_PORTABILITY_RTRIM under another driver. + */ + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result SQLite result identifier + * @access public + * @return bool true on success, false if $result is invalid + */ + function freeResult(&$result) + { + // XXX No native free? + if (!is_resource($result)) { + return false; + } + $result = null; + return true; + } + + // }}} + // {{{ numCols() + + /** + * Gets the number of columns in a result set. + * + * @return number of columns in a result set + */ + function numCols($result) + { + $cols = @sqlite_num_fields($result); + if (!$cols) { + return $this->sqliteRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Gets the number of rows affected by a query. + * + * @return number of rows affected by the last query + */ + function numRows($result) + { + $rows = @sqlite_num_rows($result); + if (!is_integer($rows)) { + return $this->raiseError(); + } + return $rows; + } + + // }}} + // {{{ affected() + + /** + * Gets the number of rows affected by a query. + * + * @return number of rows affected by the last query + */ + function affectedRows() + { + return @sqlite_changes($this->connection); + } + + // }}} + // {{{ errorNative() + + /** + * Get the native error string of the last error (if any) that + * occured on the current connection. + * + * This is used to retrieve more meaningfull error messages DB_pgsql + * way since sqlite_last_error() does not provide adequate info. + * + * @return string native SQLite error message + */ + function errorNative() + { + return($this->_lasterror); + } + + // }}} + // {{{ errorCode() + + /** + * Determine PEAR::DB error code from the database's text error message. + * + * @param string $errormsg error message returned from the database + * @return integer an error number from a DB error constant + */ + function errorCode($errormsg) + { + static $error_regexps; + if (!isset($error_regexps)) { + $error_regexps = array( + '/^no such table:/' => DB_ERROR_NOSUCHTABLE, + '/^table .* already exists$/' => DB_ERROR_ALREADY_EXISTS, + '/PRIMARY KEY must be unique/i' => DB_ERROR_CONSTRAINT, + '/is not unique/' => DB_ERROR_CONSTRAINT, + '/uniqueness constraint failed/' => DB_ERROR_CONSTRAINT, + '/may not be NULL/' => DB_ERROR_CONSTRAINT_NOT_NULL, + '/^no such column:/' => DB_ERROR_NOSUCHFIELD, + '/^near ".*": syntax error$/' => DB_ERROR_SYNTAX + ); + } + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP TABLE $seqname"); + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + $query = 'CREATE TABLE ' . $seqname . + ' (id INTEGER UNSIGNED PRIMARY KEY) '; + $result = $this->query($query); + if (DB::isError($result)) { + return($result); + } + $query = "CREATE TRIGGER ${seqname}_cleanup AFTER INSERT ON $seqname + BEGIN + DELETE FROM $seqname WHERE idquery($query); + if (DB::isError($result)) { + return($result); + } + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + + do { + $repeat = 0; + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("INSERT INTO $seqname (id) VALUES (NULL)"); + $this->popErrorHandling(); + if ($result === DB_OK) { + $id = @sqlite_last_insert_rowid($this->connection); + if ($id != 0) { + return $id; + } + } elseif ($ondemand && DB::isError($result) && + $result->getCode() == DB_ERROR_NOSUCHTABLE) + { + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } else { + $repeat = 1; + } + } + } while ($repeat); + + return $this->raiseError($result); + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info. + * + * Refer to the online manual at http://sqlite.org/sqlite.html. + * + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type, $args=array()) + { + if (!is_array($args)) + return $this->raiseError('no key specified', null, null, null, + 'Argument has to be an array.'); + switch (strtolower($type)) { + case 'master': + return 'SELECT * FROM sqlite_master;'; + case 'tables': + return "SELECT name FROM sqlite_master WHERE type='table' " + . 'UNION ALL SELECT name FROM sqlite_temp_master ' + . "WHERE type='table' ORDER BY name;"; + case 'schema': + return 'SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL ' + . 'SELECT * FROM sqlite_temp_master) ' + . "WHERE type!='meta' ORDER BY tbl_name, type DESC, name;"; + case 'schemax': + case 'schema_x': + /* + * Use like: + * $res = $db->query($db->getSpecialQuery('schema_x', array('table' => 'table3'))); + */ + return 'SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL ' + . 'SELECT * FROM sqlite_temp_master) ' + . "WHERE tbl_name LIKE '{$args['table']}' AND type!='meta' " + . 'ORDER BY type DESC, name;'; + case 'alter': + /* + * SQLite does not support ALTER TABLE; this is a helper query + * to handle this. 'table' represents the table name, 'rows' + * the news rows to create, 'save' the row(s) to keep _with_ + * the data. + * + * Use like: + * $args = array( + * 'table' => $table, + * 'rows' => "id INTEGER PRIMARY KEY, firstname TEXT, surname TEXT, datetime TEXT", + * 'save' => "NULL, titel, content, datetime" + * ); + * $res = $db->query( $db->getSpecialQuery('alter', $args)); + */ + $rows = strtr($args['rows'], $this->keywords); + + $q = array( + 'BEGIN TRANSACTION', + "CREATE TEMPORARY TABLE {$args['table']}_backup ({$args['rows']})", + "INSERT INTO {$args['table']}_backup SELECT {$args['save']} FROM {$args['table']}", + "DROP TABLE {$args['table']}", + "CREATE TABLE {$args['table']} ({$args['rows']})", + "INSERT INTO {$args['table']} SELECT {$rows} FROM {$args['table']}_backup", + "DROP TABLE {$args['table']}_backup", + 'COMMIT', + ); + + // This is a dirty hack, since the above query will no get executed with a single + // query call; so here the query method will be called directly and return a select instead. + foreach ($q as $query) { + $this->query($query); + } + return "SELECT * FROM {$args['table']};"; + default: + return null; + } + } + + // }}} + // {{{ getDbFileStats() + + /** + * Get the file stats for the current database. + * + * Possible arguments are dev, ino, mode, nlink, uid, gid, rdev, size, + * atime, mtime, ctime, blksize, blocks or a numeric key between + * 0 and 12. + * + * @param string $arg Array key for stats() + * @return mixed array on an unspecified key, integer on a passed arg and + * false at a stats error. + */ + function getDbFileStats($arg = '') + { + $stats = stat($this->dsn['database']); + if ($stats == false) { + return false; + } + if (is_array($stats)) { + if (is_numeric($arg)) { + if (((int)$arg <= 12) & ((int)$arg >= 0)) { + return false; + } + return $stats[$arg ]; + } + if (array_key_exists(trim($arg), $stats)) { + return $stats[$arg ]; + } + } + return $stats; + } + + // }}} + // {{{ escapeSimple() + + /** + * Escape a string according to the current DBMS's standards + * + * In SQLite, this makes things safe for inserts/updates, but may + * cause problems when performing text comparisons against columns + * containing binary data. See the + * {@link http://php.net/sqlite_escape_string PHP manual} for more info. + * + * @param string $str the string to be escaped + * + * @return string the escaped string + * + * @since 1.6.1 + * @see DB_common::escapeSimple() + * @internal + */ + function escapeSimple($str) { + return @sqlite_escape_string($str); + } + + // }}} + // {{{ modifyLimitQuery() + + function modifyLimitQuery($query, $from, $count, $params = array()) + { + $query = $query . " LIMIT $count OFFSET $from"; + return $query; + } + + // }}} + // {{{ modifyQuery() + + /** + * "DELETE FROM table" gives 0 affected rows in SQLite. + * + * This little hack lets you know how many rows were deleted. + * + * @param string $query The SQL query string + * @return string The SQL query string + */ + function _modifyQuery($query) + { + if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) { + if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) { + $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/', + 'DELETE FROM \1 WHERE 1=1', $query); + } + } + return $query; + } + + // }}} + // {{{ sqliteRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see errorNative() + * @see errorCode() + * @see DB_common::raiseError() + */ + function sqliteRaiseError($errno = null) + { + + $native = $this->errorNative(); + if ($errno === null) { + $errno = $this->errorCode($native); + } + + $errorcode = @sqlite_last_error($this->connection); + $userinfo = "$errorcode ** $this->last_query"; + + return $this->raiseError($errno, null, null, $userinfo, $native); + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/storage.php b/pear/DB/storage.php new file mode 100644 index 0000000..bc36533 --- /dev/null +++ b/pear/DB/storage.php @@ -0,0 +1,495 @@ + | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'DB.php'; + +/** + * Provides an object interface to a table row. + * + * It lets you add, delete and change rows using objects rather than SQL + * statements. + * + * @package DB + * @version $Id$ + * @category Database + * @author Stig Bakken + */ +class DB_storage extends PEAR +{ + // {{{ properties + + /** the name of the table (or view, if the backend database supports + updates in views) we hold data from */ + var $_table = null; + + /** which column(s) in the table contains primary keys, can be a + string for single-column primary keys, or an array of strings + for multiple-column primary keys */ + var $_keycolumn = null; + + /** DB connection handle used for all transactions */ + var $_dbh = null; + + /** an assoc with the names of database fields stored as properties + in this object */ + var $_properties = array(); + + /** an assoc with the names of the properties in this object that + have been changed since they were fetched from the database */ + var $_changes = array(); + + /** flag that decides if data in this object can be changed. + objects that don't have their table's key column in their + property lists will be flagged as read-only. */ + var $_readonly = false; + + /** function or method that implements a validator for fields that + are set, this validator function returns true if the field is + valid, false if not */ + var $_validator = null; + + // }}} + // {{{ constructor + + /** + * Constructor + * + * @param $table string the name of the database table + * + * @param $keycolumn mixed string with name of key column, or array of + * strings if the table has a primary key of more than one column + * + * @param $dbh object database connection object + * + * @param $validator mixed function or method used to validate + * each new value, called with three parameters: the name of the + * field/column that is changing, a reference to the new value and + * a reference to this object + * + */ + function DB_storage($table, $keycolumn, &$dbh, $validator = null) + { + $this->PEAR('DB_Error'); + $this->_table = $table; + $this->_keycolumn = $keycolumn; + $this->_dbh = $dbh; + $this->_readonly = false; + $this->_validator = $validator; + } + + // }}} + // {{{ _makeWhere() + + /** + * Utility method to build a "WHERE" clause to locate ourselves in + * the table. + * + * XXX future improvement: use rowids? + * + * @access private + */ + function _makeWhere($keyval = null) + { + if (is_array($this->_keycolumn)) { + if ($keyval === null) { + for ($i = 0; $i < sizeof($this->_keycolumn); $i++) { + $keyval[] = $this->{$this->_keycolumn[$i]}; + } + } + $whereclause = ''; + for ($i = 0; $i < sizeof($this->_keycolumn); $i++) { + if ($i > 0) { + $whereclause .= ' AND '; + } + $whereclause .= $this->_keycolumn[$i]; + if (is_null($keyval[$i])) { + // there's not much point in having a NULL key, + // but we support it anyway + $whereclause .= ' IS NULL'; + } else { + $whereclause .= ' = ' . $this->_dbh->quote($keyval[$i]); + } + } + } else { + if ($keyval === null) { + $keyval = @$this->{$this->_keycolumn}; + } + $whereclause = $this->_keycolumn; + if (is_null($keyval)) { + // there's not much point in having a NULL key, + // but we support it anyway + $whereclause .= ' IS NULL'; + } else { + $whereclause .= ' = ' . $this->_dbh->quote($keyval); + } + } + return $whereclause; + } + + // }}} + // {{{ setup() + + /** + * Method used to initialize a DB_storage object from the + * configured table. + * + * @param $keyval mixed the key[s] of the row to fetch (string or array) + * + * @return int DB_OK on success, a DB error if not + */ + function setup($keyval) + { + $whereclause = $this->_makeWhere($keyval); + $query = 'SELECT * FROM ' . $this->_table . ' WHERE ' . $whereclause; + $sth = $this->_dbh->query($query); + if (DB::isError($sth)) { + return $sth; + } + $row = $sth->fetchRow(DB_FETCHMODE_ASSOC); + if (DB::isError($row)) { + return $row; + } + if (!$row) { + return $this->raiseError(null, DB_ERROR_NOT_FOUND, null, null, + $query, null, true); + } + foreach ($row as $key => $value) { + $this->_properties[$key] = true; + $this->$key = $value; + } + return DB_OK; + } + + // }}} + // {{{ insert() + + /** + * Create a new (empty) row in the configured table for this + * object. + */ + function insert($newpk) + { + if (is_array($this->_keycolumn)) { + $primarykey = $this->_keycolumn; + } else { + $primarykey = array($this->_keycolumn); + } + settype($newpk, "array"); + for ($i = 0; $i < sizeof($primarykey); $i++) { + $pkvals[] = $this->_dbh->quote($newpk[$i]); + } + + $sth = $this->_dbh->query("INSERT INTO $this->_table (" . + implode(",", $primarykey) . ") VALUES(" . + implode(",", $pkvals) . ")"); + if (DB::isError($sth)) { + return $sth; + } + if (sizeof($newpk) == 1) { + $newpk = $newpk[0]; + } + $this->setup($newpk); + } + + // }}} + // {{{ toString() + + /** + * Output a simple description of this DB_storage object. + * @return string object description + */ + function toString() + { + $info = strtolower(get_class($this)); + $info .= " (table="; + $info .= $this->_table; + $info .= ", keycolumn="; + if (is_array($this->_keycolumn)) { + $info .= "(" . implode(",", $this->_keycolumn) . ")"; + } else { + $info .= $this->_keycolumn; + } + $info .= ", dbh="; + if (is_object($this->_dbh)) { + $info .= $this->_dbh->toString(); + } else { + $info .= "null"; + } + $info .= ")"; + if (sizeof($this->_properties)) { + $info .= " [loaded, key="; + $keyname = $this->_keycolumn; + if (is_array($keyname)) { + $info .= "("; + for ($i = 0; $i < sizeof($keyname); $i++) { + if ($i > 0) { + $info .= ","; + } + $info .= $this->$keyname[$i]; + } + $info .= ")"; + } else { + $info .= $this->$keyname; + } + $info .= "]"; + } + if (sizeof($this->_changes)) { + $info .= " [modified]"; + } + return $info; + } + + // }}} + // {{{ dump() + + /** + * Dump the contents of this object to "standard output". + */ + function dump() + { + foreach ($this->_properties as $prop => $foo) { + print "$prop = "; + print htmlentities($this->$prop); + print "
\n"; + } + } + + // }}} + // {{{ &create() + + /** + * Static method used to create new DB storage objects. + * @param $data assoc. array where the keys are the names + * of properties/columns + * @return object a new instance of DB_storage or a subclass of it + */ + function &create($table, &$data) + { + $classname = strtolower(get_class($this)); + $obj =& new $classname($table); + foreach ($data as $name => $value) { + $obj->_properties[$name] = true; + $obj->$name = &$value; + } + return $obj; + } + + // }}} + // {{{ loadFromQuery() + + /** + * Loads data into this object from the given query. If this + * object already contains table data, changes will be saved and + * the object re-initialized first. + * + * @param $query SQL query + * + * @param $params parameter list in case you want to use + * prepare/execute mode + * + * @return int DB_OK on success, DB_WARNING_READ_ONLY if the + * returned object is read-only (because the object's specified + * key column was not found among the columns returned by $query), + * or another DB error code in case of errors. + */ +// XXX commented out for now +/* + function loadFromQuery($query, $params = null) + { + if (sizeof($this->_properties)) { + if (sizeof($this->_changes)) { + $this->store(); + $this->_changes = array(); + } + $this->_properties = array(); + } + $rowdata = $this->_dbh->getRow($query, DB_FETCHMODE_ASSOC, $params); + if (DB::isError($rowdata)) { + return $rowdata; + } + reset($rowdata); + $found_keycolumn = false; + while (list($key, $value) = each($rowdata)) { + if ($key == $this->_keycolumn) { + $found_keycolumn = true; + } + $this->_properties[$key] = true; + $this->$key = &$value; + unset($value); // have to unset, or all properties will + // refer to the same value + } + if (!$found_keycolumn) { + $this->_readonly = true; + return DB_WARNING_READ_ONLY; + } + return DB_OK; + } + */ + + // }}} + // {{{ set() + + /** + * Modify an attriute value. + */ + function set($property, $newvalue) + { + // only change if $property is known and object is not + // read-only + if ($this->_readonly) { + return $this->raiseError(null, DB_WARNING_READ_ONLY, null, + null, null, null, true); + } + if (@isset($this->_properties[$property])) { + if (empty($this->_validator)) { + $valid = true; + } else { + $valid = @call_user_func($this->_validator, + $this->_table, + $property, + $newvalue, + $this->$property, + $this); + } + if ($valid) { + $this->$property = $newvalue; + if (empty($this->_changes[$property])) { + $this->_changes[$property] = 0; + } else { + $this->_changes[$property]++; + } + } else { + return $this->raiseError(null, DB_ERROR_INVALID, null, + null, "invalid field: $property", + null, true); + } + return true; + } + return $this->raiseError(null, DB_ERROR_NOSUCHFIELD, null, + null, "unknown field: $property", + null, true); + } + + // }}} + // {{{ &get() + + /** + * Fetch an attribute value. + * + * @param string attribute name + * + * @return attribute contents, or null if the attribute name is + * unknown + */ + function &get($property) + { + // only return if $property is known + if (isset($this->_properties[$property])) { + return $this->$property; + } + $tmp = null; + return $tmp; + } + + // }}} + // {{{ _DB_storage() + + /** + * Destructor, calls DB_storage::store() if there are changes + * that are to be kept. + */ + function _DB_storage() + { + if (sizeof($this->_changes)) { + $this->store(); + } + $this->_properties = array(); + $this->_changes = array(); + $this->_table = null; + } + + // }}} + // {{{ store() + + /** + * Stores changes to this object in the database. + * + * @return DB_OK or a DB error + */ + function store() + { + foreach ($this->_changes as $name => $foo) { + $params[] = &$this->$name; + $vars[] = $name . ' = ?'; + } + if ($vars) { + $query = 'UPDATE ' . $this->_table . ' SET ' . + implode(', ', $vars) . ' WHERE ' . + $this->_makeWhere(); + $stmt = $this->_dbh->prepare($query); + $res = $this->_dbh->execute($stmt, $params); + if (DB::isError($res)) { + return $res; + } + $this->_changes = array(); + } + return DB_OK; + } + + // }}} + // {{{ remove() + + /** + * Remove the row represented by this object from the database. + * + * @return mixed DB_OK or a DB error + */ + function remove() + { + if ($this->_readonly) { + return $this->raiseError(null, DB_WARNING_READ_ONLY, null, + null, null, null, true); + } + $query = 'DELETE FROM ' . $this->_table .' WHERE '. + $this->_makeWhere(); + $res = $this->_dbh->query($query); + if (DB::isError($res)) { + return $res; + } + foreach ($this->_properties as $prop => $foo) { + unset($this->$prop); + } + $this->_properties = array(); + $this->_changes = array(); + return DB_OK; + } + + // }}} +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?> diff --git a/pear/DB/sybase.php b/pear/DB/sybase.php new file mode 100644 index 0000000..93e4e79 --- /dev/null +++ b/pear/DB/sybase.php @@ -0,0 +1,835 @@ + | +// | Antônio Carlos Venâncio Júnior | +// | Maintainer: Daniel Convissor | +// +----------------------------------------------------------------------+ +// +// $Id$ + + +// TODO +// - This driver may fail with multiple connections under the same +// user/pass/host and different databases + + +require_once 'DB/common.php'; + +/** + * Database independent query interface definition for PHP's Sybase + * extension. + * + * @package DB + * @version $Id$ + * @category Database + * @author Sterling Hughes + * @author Antônio Carlos Venâncio Júnior + */ +class DB_sybase extends DB_common +{ + // {{{ properties + + var $connection; + var $phptype, $dbsyntax; + var $prepare_tokens = array(); + var $prepare_types = array(); + var $transaction_opcount = 0; + var $autocommit = true; + + // }}} + // {{{ constructor + + /** + * DB_sybase constructor. + * + * @access public + */ + function DB_sybase() + { + $this->DB_common(); + $this->phptype = 'sybase'; + $this->dbsyntax = 'sybase'; + $this->features = array( + 'prepare' => false, + 'pconnect' => true, + 'transactions' => false, + 'limit' => 'emulate' + ); + $this->errorcode_map = array( + ); + } + + // }}} + // {{{ connect() + + /** + * Connect to a database and log in as the specified user. + * + * @param $dsn the data source name (see DB::parseDSN for syntax) + * @param $persistent (optional) whether the connection should + * be persistent + * @access public + * @return int DB_OK on success, a DB error on failure + */ + function connect($dsninfo, $persistent = false) + { + if (!DB::assertExtension('sybase') && + !DB::assertExtension('sybase_ct')) + { + return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND); + } + + $this->dsn = $dsninfo; + + $interface = $dsninfo['hostspec'] ? $dsninfo['hostspec'] : 'localhost'; + $connect_function = $persistent ? 'sybase_pconnect' : 'sybase_connect'; + $dsninfo['password'] = !empty($dsninfo['password']) ? $dsninfo['password'] : false; + $dsninfo['charset'] = isset($dsninfo['charset']) ? $dsninfo['charset'] : false; + $dsninfo['appname'] = isset($dsninfo['appname']) ? $dsninfo['appname'] : false; + + if ($interface && $dsninfo['username']) { + $conn = @$connect_function($interface, $dsninfo['username'], + $dsninfo['password'], + $dsninfo['charset'], + $dsninfo['appname']); + } else { + $conn = false; + } + + if (!$conn) { + return $this->raiseError(DB_ERROR_CONNECT_FAILED); + } + + if ($dsninfo['database']) { + if (!@sybase_select_db($dsninfo['database'], $conn)) { + return $this->raiseError(DB_ERROR_NODBSELECTED, null, + null, null, @sybase_get_last_message()); + } + $this->_db = $dsninfo['database']; + } + + $this->connection = $conn; + return DB_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @access public + * + * @return bool true on success, false if not connected. + */ + function disconnect() + { + $ret = @sybase_close($this->connection); + $this->connection = null; + return $ret; + } + + // }}} + // {{{ errorNative() + + /** + * Get the last server error messge (if any) + * + * @return string sybase last error message + */ + function errorNative() + { + return @sybase_get_last_message(); + } + + // }}} + // {{{ errorCode() + + /** + * Determine PEAR::DB error code from the database's text error message. + * + * @param string $errormsg error message returned from the database + * @return integer an error number from a DB error constant + */ + function errorCode($errormsg) + { + static $error_regexps; + if (!isset($error_regexps)) { + $error_regexps = array( + '/Incorrect syntax near/' + => DB_ERROR_SYNTAX, + '/^Unclosed quote before the character string [\"\'].*[\"\']\./' + => DB_ERROR_SYNTAX, + '/Implicit conversion from datatype [\"\'].+[\"\'] to [\"\'].+[\"\'] is not allowed\./' + => DB_ERROR_INVALID_NUMBER, + '/Cannot drop the table [\"\'].+[\"\'], because it doesn\'t exist in the system catalogs\./' + => DB_ERROR_NOSUCHTABLE, + '/Only the owner of object [\"\'].+[\"\'] or a user with System Administrator \(SA\) role can run this command\./' + => DB_ERROR_ACCESS_VIOLATION, + '/^.+ permission denied on object .+, database .+, owner .+/' + => DB_ERROR_ACCESS_VIOLATION, + '/^.* permission denied, database .+, owner .+/' + => DB_ERROR_ACCESS_VIOLATION, + '/[^.*] not found\./' + => DB_ERROR_NOSUCHTABLE, + '/There is already an object named/' + => DB_ERROR_ALREADY_EXISTS, + '/Invalid column name/' + => DB_ERROR_NOSUCHFIELD, + '/does not allow null values/' + => DB_ERROR_CONSTRAINT_NOT_NULL, + '/Command has been aborted/' + => DB_ERROR_CONSTRAINT, + ); + } + + foreach ($error_regexps as $regexp => $code) { + if (preg_match($regexp, $errormsg)) { + return $code; + } + } + return DB_ERROR; + } + + // }}} + // {{{ sybaseRaiseError() + + /** + * Gather information about an error, then use that info to create a + * DB error object and finally return that object. + * + * @param integer $errno PEAR error number (usually a DB constant) if + * manually raising an error + * @return object DB error object + * @see errorNative() + * @see errorCode() + * @see DB_common::raiseError() + */ + function sybaseRaiseError($errno = null) + { + $native = $this->errorNative(); + if ($errno === null) { + $errno = $this->errorCode($native); + } + return $this->raiseError($errno, null, null, null, $native); + } + + // }}} + // {{{ simpleQuery() + + /** + * Send a query to Sybase and return the results as a Sybase resource + * identifier. + * + * @param the SQL query + * + * @access public + * + * @return mixed returns a valid Sybase result for successful SELECT + * queries, DB_OK for other successful queries. A DB error is + * returned on failure. + */ + function simpleQuery($query) + { + $ismanip = DB::isManip($query); + $this->last_query = $query; + if (!@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $query = $this->modifyQuery($query); + if (!$this->autocommit && $ismanip) { + if ($this->transaction_opcount == 0) { + $result = @sybase_query('BEGIN TRANSACTION', $this->connection); + if (!$result) { + return $this->sybaseRaiseError(); + } + } + $this->transaction_opcount++; + } + $result = @sybase_query($query, $this->connection); + if (!$result) { + return $this->sybaseRaiseError(); + } + if (is_resource($result)) { + $numrows = $this->numRows($result); + if (is_object($numrows)) { + return $numrows; + } + $this->num_rows[(int)$result] = $numrows; + return $result; + } + // Determine which queries that should return data, and which + // should return an error code only. + return $ismanip ? DB_OK : $result; + } + + // }}} + // {{{ nextResult() + + /** + * Move the internal sybase result pointer to the next available result + * + * @param a valid sybase result resource + * + * @access public + * + * @return true if a result is available otherwise return false + */ + function nextResult($result) + { + return false; + } + + // }}} + // {{{ fetchInto() + + /** + * Fetch a row and insert the data into an existing array. + * + * Formating of the array and the data therein are configurable. + * See DB_result::fetchInto() for more information. + * + * @param resource $result query result identifier + * @param array $arr (reference) array where data from the row + * should be placed + * @param int $fetchmode how the resulting array should be indexed + * @param int $rownum the row number to fetch + * + * @return mixed DB_OK on success, null when end of result set is + * reached or on failure + * + * @see DB_result::fetchInto() + * @access private + */ + function fetchInto($result, &$arr, $fetchmode, $rownum=null) + { + if ($rownum !== null) { + if (!@sybase_data_seek($result, $rownum)) { + return null; + } + } + if ($fetchmode & DB_FETCHMODE_ASSOC) { + if (function_exists('sybase_fetch_assoc')) { + $arr = @sybase_fetch_assoc($result); + } else { + if ($arr = @sybase_fetch_array($result)) { + foreach ($arr as $key => $value) { + if (is_int($key)) { + unset($arr[$key]); + } + } + } + } + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) { + $arr = array_change_key_case($arr, CASE_LOWER); + } + } else { + $arr = @sybase_fetch_row($result); + } + if (!$arr) { + // reported not work as seems that sybase_get_last_message() + // always return a message here + //if ($errmsg = @sybase_get_last_message()) { + // return $this->sybaseRaiseError($errmsg); + //} else { + return null; + //} + } + if ($this->options['portability'] & DB_PORTABILITY_RTRIM) { + $this->_rtrimArrayValues($arr); + } + if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) { + $this->_convertNullArrayValuesToEmpty($arr); + } + return DB_OK; + } + + // }}} + // {{{ freeResult() + + /** + * Free the internal resources associated with $result. + * + * @param $result Sybase result identifier + * + * @access public + * + * @return bool true on success, false if $result is invalid + */ + function freeResult($result) + { + unset($this->num_rows[(int)$result]); + return @sybase_free_result($result); + } + + // }}} + // {{{ numCols() + + /** + * Get the number of columns in a result set. + * + * @param $result Sybase result identifier + * + * @access public + * + * @return int the number of columns per row in $result + */ + function numCols($result) + { + $cols = @sybase_num_fields($result); + if (!$cols) { + return $this->sybaseRaiseError(); + } + return $cols; + } + + // }}} + // {{{ numRows() + + /** + * Get the number of rows in a result set. + * + * @param $result Sybase result identifier + * + * @access public + * + * @return int the number of rows in $result + */ + function numRows($result) + { + $rows = @sybase_num_rows($result); + if ($rows === false) { + return $this->sybaseRaiseError(); + } + return $rows; + } + + // }}} + // {{{ affectedRows() + + /** + * Gets the number of rows affected by the data manipulation + * query. For other queries, this function returns 0. + * + * @return number of rows affected by the last query + */ + function affectedRows() + { + if (DB::isManip($this->last_query)) { + $result = @sybase_affected_rows($this->connection); + } else { + $result = 0; + } + return $result; + } + + // }}} + // {{{ nextId() + + /** + * Returns the next free id in a sequence + * + * @param string $seq_name name of the sequence + * @param boolean $ondemand when true, the seqence is automatically + * created if it does not exist + * + * @return int the next id number in the sequence. DB_Error if problem. + * + * @internal + * @see DB_common::nextID() + * @access public + */ + function nextId($seq_name, $ondemand = true) + { + $seqname = $this->getSequenceName($seq_name); + if (!@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $repeat = 0; + do { + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)"); + $this->popErrorHandling(); + if ($ondemand && DB::isError($result) && + ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE)) + { + $repeat = 1; + $result = $this->createSequence($seq_name); + if (DB::isError($result)) { + return $this->raiseError($result); + } + } elseif (!DB::isError($result)) { + $result =& $this->query("SELECT @@IDENTITY FROM $seqname"); + $repeat = 0; + } else { + $repeat = false; + } + } while ($repeat); + if (DB::isError($result)) { + return $this->raiseError($result); + } + $result = $result->fetchRow(DB_FETCHMODE_ORDERED); + return $result[0]; + } + + /** + * Creates a new sequence + * + * @param string $seq_name name of the new sequence + * + * @return int DB_OK on success. A DB_Error object is returned if + * problems arise. + * + * @internal + * @see DB_common::createSequence() + * @access public + */ + function createSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("CREATE TABLE $seqname ". + '(id numeric(10,0) IDENTITY NOT NULL ,' . + 'vapor int NULL)'); + } + + // }}} + // {{{ dropSequence() + + /** + * Deletes a sequence + * + * @param string $seq_name name of the sequence to be deleted + * + * @return int DB_OK on success. DB_Error if problems. + * + * @internal + * @see DB_common::dropSequence() + * @access public + */ + function dropSequence($seq_name) + { + $seqname = $this->getSequenceName($seq_name); + return $this->query("DROP TABLE $seqname"); + } + + // }}} + // {{{ getSpecialQuery() + + /** + * Returns the query needed to get some backend info + * @param string $type What kind of info you want to retrieve + * @return string The SQL query string + */ + function getSpecialQuery($type) + { + switch ($type) { + case 'tables': + return "select name from sysobjects where type = 'U' order by name"; + case 'views': + return "select name from sysobjects where type = 'V'"; + default: + return null; + } + } + + // }}} + // {{{ autoCommit() + + /** + * Enable/disable automatic commits + */ + function autoCommit($onoff = false) + { + // XXX if $this->transaction_opcount > 0, we should probably + // issue a warning here. + $this->autocommit = $onoff ? true : false; + return DB_OK; + } + + // }}} + // {{{ commit() + + /** + * Commit the current transaction. + */ + function commit() + { + if ($this->transaction_opcount > 0) { + if (!@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @sybase_query('COMMIT', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->sybaseRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ rollback() + + /** + * Roll back (undo) the current transaction. + */ + function rollback() + { + if ($this->transaction_opcount > 0) { + if (!@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $result = @sybase_query('ROLLBACK', $this->connection); + $this->transaction_opcount = 0; + if (!$result) { + return $this->sybaseRaiseError(); + } + } + return DB_OK; + } + + // }}} + // {{{ tableInfo() + + /** + * Returns information about a table or a result set. + * + * NOTE: only supports 'table' and 'flags' if $result + * is a table name. + * + * @param object|string $result DB_result object from a query or a + * string containing the name of a table + * @param int $mode a valid tableInfo mode + * @return array an associative array with the information requested + * or an error object if something is wrong + * @access public + * @internal + * @since 1.6.0 + * @see DB_common::tableInfo() + */ + function tableInfo($result, $mode = null) + { + if (isset($result->result)) { + /* + * Probably received a result object. + * Extract the result resource identifier. + */ + $id = $result->result; + $got_string = false; + } elseif (is_string($result)) { + /* + * Probably received a table name. + * Create a result resource identifier. + */ + if (!@sybase_select_db($this->_db, $this->connection)) { + return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED); + } + $id = @sybase_query("SELECT * FROM $result WHERE 1=0", + $this->connection); + $got_string = true; + } else { + /* + * Probably received a result resource identifier. + * Copy it. + * Deprecated. Here for compatibility only. + */ + $id = $result; + $got_string = false; + } + + if (!is_resource($id)) { + return $this->sybaseRaiseError(DB_ERROR_NEED_MORE_DATA); + } + + if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) { + $case_func = 'strtolower'; + } else { + $case_func = 'strval'; + } + + $count = @sybase_num_fields($id); + + // made this IF due to performance (one if is faster than $count if's) + if (!$mode) { + + for ($i=0; $i<$count; $i++) { + $f = @sybase_fetch_field($id, $i); + + // column_source is often blank + if ($got_string) { + $res[$i]['table'] = $case_func($result); + } else { + $res[$i]['table'] = $case_func($f->column_source); + } + $res[$i]['name'] = $case_func($f->name); + $res[$i]['type'] = $f->type; + $res[$i]['len'] = $f->max_length; + if ($res[$i]['table']) { + $res[$i]['flags'] = $this->_sybase_field_flags( + $res[$i]['table'], $res[$i]['name']); + } else { + $res[$i]['flags'] = ''; + } + } + + } else { + // get full info + + $res['num_fields'] = $count; + + for ($i=0; $i<$count; $i++) { + $f = @sybase_fetch_field($id, $i); + + // column_source is often blank + if ($got_string) { + $res[$i]['table'] = $case_func($result); + } else { + $res[$i]['table'] = $case_func($f->column_source); + } + $res[$i]['name'] = $case_func($f->name); + $res[$i]['type'] = $f->type; + $res[$i]['len'] = $f->max_length; + if ($res[$i]['table']) { + $res[$i]['flags'] = $this->_sybase_field_flags( + $res[$i]['table'], $res[$i]['name']); + } else { + $res[$i]['flags'] = ''; + } + + if ($mode & DB_TABLEINFO_ORDER) { + $res['order'][$res[$i]['name']] = $i; + } + if ($mode & DB_TABLEINFO_ORDERTABLE) { + $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i; + } + } + } + + // free the result only if we were called on a table + if ($got_string) { + @sybase_free_result($id); + } + return $res; + } + + // }}} + // {{{ _sybase_field_flags() + + /** + * Get the flags for a field. + * + * Currently supports: + * + unique_key (unique index, unique check or primary_key) + * + multiple_key (multi-key index) + * + * @param string $table table name + * @param string $column field name + * @return string space delimited string of flags. Empty string if none. + * @access private + */ + function _sybase_field_flags($table, $column) + { + static $tableName = null; + static $flags = array(); + + if ($table != $tableName) { + $flags = array(); + $tableName = $table; + + // get unique/primary keys + $res = $this->getAll("sp_helpindex $table", DB_FETCHMODE_ASSOC); + + if (!isset($res[0]['index_description'])) { + return ''; + } + + foreach ($res as $val) { + $keys = explode(', ', trim($val['index_keys'])); + + if (sizeof($keys) > 1) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'multiple_key'); + } + } + + if (strpos($val['index_description'], 'unique')) { + foreach ($keys as $key) { + $this->_add_flag($flags[$key], 'unique_key'); + } + } + } + + } + + if (array_key_exists($column, $flags)) { + return(implode(' ', $flags[$column])); + } + + return ''; + } + + // }}} + // {{{ _add_flag() + + /** + * Adds a string to the flags array if the flag is not yet in there + * - if there is no flag present the array is created. + * + * @param array $array reference of flags array to add a value to + * @param mixed $value value to add to the flag array + * @access private + */ + function _add_flag(&$array, $value) + { + if (!is_array($array)) { + $array = array($value); + } elseif (!in_array($value, $array)) { + array_push($array, $value); + } + } + + // }}} + // {{{ quoteIdentifier() + + /** + * Quote a string so it can be safely used as a table / column name + * + * Quoting style depends on which database driver is being used. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + * + * @since 1.6.0 + * @access public + */ + function quoteIdentifier($str) + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + // }}} + +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ + +?>