. */ require_once 'creole/Connection.php'; require_once 'creole/common/ConnectionCommon.php'; require_once 'creole/drivers/odbc/adapters/ODBCAdapter.php'; /** * ODBC implementation of Connection. * * @author Dave Lawson * @version $Revision: 1.6 $ * @package creole.drivers.odbc */ class ODBCConnection extends ConnectionCommon implements Connection { /** * Implements driver-specific behavior * @var ODBCAdapter */ protected $adapter = null; /** * Last ODBC result resource from executeQuery/executeUpdate. Used in getUpdateCount() * @var ODBCResultResource */ protected $odbcresult = null; /** * @see Connection::connect() */ public function connect($dsninfo, $flags = 0) { if (!function_exists('odbc_connect')) throw new SQLException('odbc extension not loaded'); $adapterclass = isset($dsninfo['adapter']) ? $dsninfo['adapter'] : null; if (!$adapterclass) $adapterclass = 'ODBCAdapter'; else $adapterclass .= 'Adapter'; Creole::import('creole.drivers.odbc.adapters.' . $adapterclass); $this->adapter = new $adapterclass(); $this->dsn = $dsninfo; $this->flags = $flags; if ( !($this->flags & Creole::COMPAT_ASSOC_LOWER) && !$this->adapter->preservesColumnCase()) { trigger_error('Connection created without Creole::COMPAT_ASSOC_LOWER, ' . 'but driver does not support case preservation.', E_USER_WARNING); $this->flags != Creole::COMPAT_ASSOC_LOWER; } $persistent = ($flags & Creole::PERSISTENT) === Creole::PERSISTENT; if ($dsninfo['database']) $odbcdsn = $dsninfo['database']; elseif ($dsninfo['hostspec']) $odbcdsn = $dsninfo['hostspec']; else $odbcdsn = 'localhost'; $user = @$dsninfo['username']; $pw = @$dsninfo['password']; $connect_function = $persistent ? 'odbc_pconnect' : 'odbc_connect'; $conn = @$connect_function($odbcdsn, $user, $pw, SQL_CUR_USE_IF_NEEDED); if (!is_resource($conn)) throw new SQLException('connect failed', $this->nativeError(), $odbcdsn); $this->dblink = $conn; /** * This prevents blob fields from being fetched when a row is loaded * from a recordset. Clob fields however are loaded with up to * 'odbc.defaultlrl' data. This should be the default anyway, but we'll * set it here just to keep things consistent. */ @odbc_binmode(0, ODBC_BINMODE_PASSTHRU); @odbc_longreadlen(0, ini_get('odbc.defaultlrl')); } /** * @see Connection::close() */ public function close() { $ret = true; $this->adapter = null; $this->odbcresult = null; if ($this->dblink !== null) { $ret = @odbc_close($this->dblink); $this->dblink = null; } return $ret; } /** * Shouldn't this be in ConnectionCommon.php? */ public function __destruct() { $this->close(); } /** * Returns a formatted ODBC error string. * @return string */ public function nativeError() { if ($this->dblink && is_resource($this->dblink)) $errstr = '[' . @odbc_error($this->dblink) . '] ' . @odbc_errormsg($this->dblink); else $errstr = '[' . @odbc_error() . '] ' . @odbc_errormsg(); return $errstr; } /** * Returns driver-specific ODBCAdapter. * @return ODBCAdapter */ public function getAdapter() { return $this->adapter; } /** * @see Connection::getDatabaseInfo() */ public function getDatabaseInfo() { require_once 'creole/drivers/odbc/metadata/ODBCDatabaseInfo.php'; return new ODBCDatabaseInfo($this); } /** * @see Connection::getIdGenerator() */ public function getIdGenerator() { return $this->adapter->getIdGenerator($this); } /** * Creates the appropriate ResultSet * @return ResultSet */ public function createResultSet($odbcresult, $fetchmode) { return $this->adapter->createResultSet($this, $odbcresult, $fetchmode); } /** * @see Connection::prepareStatement() */ public function prepareStatement($sql) { require_once 'creole/drivers/odbc/ODBCPreparedStatement.php'; return new ODBCPreparedStatement($this, $sql); } /** * @see Connection::createStatement() */ public function createStatement() { require_once 'creole/drivers/odbc/ODBCStatement.php'; return new ODBCStatement($this); } /** * @todo To be implemented * @see Connection::prepareCall() */ public function prepareCall($sql) { throw new SQLException('Stored procedures not currently implemented.'); } /** * @see Connection::applyLimit() */ public function applyLimit(&$sql, $offset, $limit) { if ($this->adapter->hasLimitOffset()) $this->adapter->applyLimit($sql, $offset, $limit); } /** * @see Connection::executeQuery() */ public function executeQuery($sql, $fetchmode = null) { if ($this->odbcresult) $this->odbcresult = null; $r = @odbc_exec($this->dblink, $sql); if ($r === false) throw new SQLException('Could not execute query', $this->nativeError(), $sql); $this->odbcresult = new ODBCResultResource($r); return $this->createResultSet($this->odbcresult, $fetchmode); } /** * @see Connection::executeUpdate() */ public function executeUpdate($sql) { if ($this->odbcresult) $this->odbcresult = null; $r = @odbc_exec($this->dblink, $sql); if ($r === false) throw new SQLException('Could not execute update', $this->nativeError(), $sql); $this->odbcresult = new ODBCResultResource($r); return $this->getUpdateCount(); } /** * Start a database transaction. * @throws SQLException * @return void */ protected function beginTrans() { if ($this->adapter->supportsTransactions()) { @odbc_autocommit($this->dblink, false); if (odbc_error($this->dblink) == 'S1C00') { throw new SQLException('Could not begin transaction', $this->nativeError()); } } } /** * Commit the current transaction. * @throws SQLException * @return void */ protected function commitTrans() { if ($this->adapter->supportsTransactions()) { $result = @odbc_commit($this->dblink); if (!$result) { throw new SQLException('Could not commit transaction', $this->nativeError()); } @odbc_autocommit($this->dblink, true); if (odbc_error($this->dblink) == 'S1C00') { throw new SQLException('Could not commit transaction (autocommit failed)', $this->nativeError()); } } } /** * Roll back (undo) the current transaction. * @throws SQLException * @return void */ protected function rollbackTrans() { if ($this->adapter->supportsTransactions()) { $result = @odbc_rollback($this->dblink); if (!$result) { throw new SQLException('Could not rollback transaction', $this->nativeError()); } @odbc_autocommit($this->dblink, true); if (odbc_error($this->dblink) == 'S1C00') { throw new SQLException('Could not rollback transaction (autocommit failed)', $this->nativeError()); } } } /** * @see Connection::getUpdateCount() */ public function getUpdateCount() { if ($this->odbcresult === null) return 0; $n = @odbc_num_rows($this->odbcresult->getHandle()); if ($n == -1) throw new SQLException('Could not retrieve update count', $this->nativeError()); return (int) $n; } } /** * This is a simple wrapper class to manage the lifetime of an ODBC result resource * (returned by odbc_exec(), odbc_execute(), etc.) We use a separate class because * the resource can be shared by both ODBCConnection and an ODBCResultSet at the * same time. ODBCConnection hangs on to the last result resource to be used in * its getUpdateCount() method. It also passes this resource to new instances of * ODBCResultSet. At some point the resource has to be cleaned up via * odbc_free_result(). Using this class as a wrapper, we can pass around multiple * references to the same resource. PHP's reference counting mechanism will clean * up the resource when its no longer used via ODBCResultResource::__destruct(). * @package creole.drivers.odbc */ class ODBCResultResource { /** * @var resource ODBC result resource returned by {@link odbc_exec()}/{@link odbc_execute()}. */ protected $handle = null; public function __construct($handle) { if (is_resource($handle)) $this->handle = $handle; } public function __destruct() { if ($this->handle !== null) @odbc_free_result($this->handle); } public function getHandle() { return $this->handle; } }