. */ require_once 'creole/PreparedStatement.php'; require_once 'creole/common/PreparedStatementCommon.php'; /** * Oracle (OCI8) implementation of PreparedStatement. * * @author David Giffin * @author Hans Lellelid * @version $Revision: 1.26 $ * @package creole.drivers.oracle */ class OCI8PreparedStatement extends PreparedStatementCommon implements PreparedStatement { /** * Descriptor holders for LOB values. * There are other types of descriptors, but we need to keep * them separate, because we need to execute the save()/savefile() method * on lob descriptors. * @var array object from oci_new_descriptor */ private $lobDescriptors = array(); /** * Hold any Blob/Clob data. * These can be matched (by key) to descriptors in $lobDescriptors. * @var array Lob[] */ private $lobs = array(); /** * Array to store the columns in an insert or update statement. * This is necessary for the proper handling of lob variables * @var arrary columns[] */ private $columns = array(); /** * If the statement is set, free it. * @see PreparedStatement::close() */ function close() { if (isset($this->stmt)) @oci_free_statement($this->stmt); } /** * Nothing to do - since oci_bind is used to insert data, no escaping is needed * @param string $str * @return string */ protected function escape($str) { return $str; } /** * Executes the SQL query in this PreparedStatement object and returns the resultset generated by the query. * @param mixed $p1 Either (array) Parameters that will be set using PreparedStatement::set() before query is executed or (int) fetchmode. * @param int $fetchmode The mode to use when fetching the results (e.g. ResultSet::FETCHMODE_NUM, ResultSet::FETCHMODE_ASSOC). * @return ResultSet * @throws SQLException if a database access error occurs. */ public function executeQuery($p1 = null, $fetchmode = null) { $params = null; if ($fetchmode !== null) { $params = $p1; } elseif ($p1 !== null) { if (is_array($p1)) $params = $p1; else $fetchmode = $p1; } if ($params) { for($i=0,$cnt=count($params); $i < $cnt; $i++) { $this->set($i+1, $params[$i]); } } $this->updateCount = null; // reset $sql = $this->sqlToOracleBindVars($this->sql); if ($this->limit > 0 || $this->offset > 0) { $this->conn->applyLimit($sql, $this->offset, $this->limit); } $result = oci_parse($this->conn->getResource(), $sql); if (!$result) { throw new SQLException("Unable to prepare query", $this->conn->nativeError(), $this->sqlToOracleBindVars($this->sql)); } // bind all variables $this->bindVars($result); $success = oci_execute($result, OCI_DEFAULT); if (!$success) { throw new SQLException("Unable to execute query", $this->conn->nativeError($result), $this->sqlToOracleBindVars($this->sql)); } $this->resultSet = new OCI8ResultSet($this->conn, $result, $fetchmode); return $this->resultSet; } /** * Executes the SQL INSERT, UPDATE, or DELETE statement in this PreparedStatement object. * * @param array $params Parameters that will be set using PreparedStatement::set() before query is executed. * @return int Number of affected rows (or 0 for drivers that return nothing). * @throws SQLException if a database access error occurs. */ public function executeUpdate($params = null) { if ($params) { for($i=0,$cnt=count($params); $i < $cnt; $i++) { $this->set($i+1, $params[$i]); } } if($this->resultSet) $this->resultSet->close(); $this->resultSet = null; // reset $stmt = oci_parse($this->conn->getResource(), $this->sqlToOracleBindVars($this->sql)); if (!$stmt) { throw new SQLException("Unable to prepare update", $this->conn->nativeError(), $this->sqlToOracleBindVars($this->sql)); } // bind all variables $this->bindVars($stmt); // Even if autocommit is on, delay commit until after LOBS have been saved $success = oci_execute($stmt, OCI_DEFAULT); if (!$success) { throw new SQLException("Unable to execute update", $this->conn->nativeError($stmt), $this->sqlToOracleBindVars($this->sql)); } // save data in any LOB descriptors, then free them foreach($this->lobDescriptors as $paramIndex => $lobster) { $lob = $this->lobs[$paramIndex]; // corresponding Blob/Clob if ($lob->isFromFile()) { $success = $lobster->savefile($lob->getInputFile()); } else { $success = $lobster->save($lob->getContents()); } if (!$success) { $lobster->free(); throw new SQLException("Error saving lob bound to " . $paramIndex); } $lobster->free(); } if ($this->conn->getAutoCommit()) { oci_commit($this->conn->getResource()); // perform deferred commit } $this->updateCount = @oci_num_rows($stmt); return $this->updateCount; } /** * Performs the actual binding of variables using oci_bind_by_name(). * * This may seem like useless overhead, but the reason why calls to oci_bind_by_name() * are not performed in the set*() methods is that it is possible that the SQL will * need to be modified -- e.g. by a setLimit() call -- and re-prepared. We cannot assume * that the statement has been prepared when the set*() calls are invoked. This also means, * therefore, that the set*() calls will not throw exceptions; all exceptions will be thrown * when the statement is prepared. * * @param resource $stmt The statement result of oci_parse to use for binding. * @return void */ private function bindVars($stmt) { foreach ($this->boundInVars as $idx => $val) { $idxName = ":var" . $idx; if (!oci_bind_by_name($stmt, $idxName, $this->boundInVars[$idx], -1)) { throw new SQLException("Erorr binding value to placeholder " . $idx); } } // foreach foreach ($this->lobs as $idx => $val) { $idxName = ":var" . $idx; if (class_exists('Blob') && $val instanceof Blob){ if (!oci_bind_by_name($stmt, $idxName, $this->lobDescriptors[$idx], -1, OCI_B_BLOB)) throw new SQLException("Erorr binding blob to placeholder " . $idx); } elseif (class_exists('Clob') && $val instanceof Clob){ if (!oci_bind_by_name($stmt, $idxName, $this->lobDescriptors[$idx], -1, OCI_B_CLOB)) throw new SQLException("Erorr binding clob to placeholder " . $idx); } } // foreach } /** * Convert a Propel SQL into Oracle SQL * * Look for all of the '?' and replace with ":varX" * * @param string $sql SQL in Propel native format * @return string SQL in Oracle Bind Var format * @todo -cOCI8PreparedStatement Consider changing this implementation to use the fact that we * already know where all the '?' chars are (in $positions array). */ private function sqlToOracleBindVars($sql) { $out = ""; $in_literal = 0; $idxNum = 1; for ($i = 0; $i < strlen($sql); $i++) { $char = $sql[$i]; if (strcmp($char,"'")==0) { $in_literal = ~$in_literal; } if (strcmp($char,"?")==0 && !$in_literal) { if (array_key_exists($idxNum, $this->lobs)){ if (class_exists('Blob') && ($this->lobs[$idxNum] instanceof Blob)) $out .= "empty_blob()"; if (class_exists('Clob') && ($this->lobs[$idxNum] instanceof Clob)) $out .= "empty_clob()"; } else $out .= ":var" . $idxNum; $idxNum++; } else { $out .= $char; } } if (isset($this->lobs) && !empty($this->lobs)) { $this->setColumnArray(); $retstmt = " Returning "; $collist = ""; $bindlist = ""; foreach ($this->lobs as $idx=>$val) { $idxName = ":var" . $idx; if ((class_exists('Blob') && $val instanceof Blob) || (class_exists('Clob') && $val instanceof Clob)) { //the columns array starts at zero instead of 1 like the lobs array $collist .= $this->columns[$idx-1] . ","; $bindlist .= $idxName . ","; } } if (!empty($collist)) $out .= $retstmt . rtrim($collist, ",") . " into " . rtrim($bindlist, ","); } return $out; } /** * @param string $paramIndex * @param mixed $blob Blob object or string containing data. * @return void */ function setBlob($paramIndex, $blob) { require_once 'creole/util/Blob.php'; if (!($blob instanceof Blob)) { $b = new Blob(); $b->setContents($blob); $blob = $b; } $this->lobDescriptors[$paramIndex] = oci_new_descriptor($this->conn->getResource(), OCI_D_LOB); $this->lobs[$paramIndex] = $blob; } /** * @param string $paramIndex * @param mixed $clob Clob object or string containing data. * @return void */ function setClob($paramIndex, $clob) { require_once 'creole/util/Clob.php'; if (!($clob instanceof Clob)) { $c = new Clob(); $c->setContents($clob); $clob = $c; } $this->lobDescriptors[$paramIndex] = oci_new_descriptor($this->conn->getResource(), OCI_D_LOB); $this->lobs[$paramIndex] = $clob; } /** * Since bind variables in oracle have no special characters, this setString method differs from the * common one in that it does not single quote strings. * * @param int $paramIndex * @param string $value * @return void */ function setString($paramIndex, $value) { if ($value === null) { $this->setNull($paramIndex); } else { // it's ok to have a fatal error here, IMO, if object doesn't have // __toString() and is being passed to this method. if ( is_object ( $value ) ) { $this->boundInVars[$paramIndex] = $value->__toString(); } else { $this->boundInVars[$paramIndex] = (string)$value; } } } /** * Copied this function from common/PreparedStatement.php and modified to work with Oracle * Please note the format used with date() matches that of NLS_DATE_FORMAT set in * OCI8Connection.php * * @param int $paramIndex * @param string $value * @return void */ function setTimestamp($paramIndex, $value) { if ($value === null) { $this->setNull($paramIndex); } else { if (is_numeric($value)) $value = date('Y-m-d H:i:s', $value); elseif (is_object($value)) $value = date('Y-m-d H:i:s', $value->getTime()); $this->boundInVars[$paramIndex] = $value; } } /** * Please note the format used with date() matches that of NLS_DATE_FORMAT set in * OCI8Connection.php * * @param int $paramIndex * @param string $value * @return void */ function setDate($paramIndex, $value) { if ($value === null) { $this->setNull($paramIndex); } else { if (is_numeric($value)) $value = date("Y-m-d", $value); elseif (is_object($value)) $value = date("Y-m-d", $value->getTime()); $this->boundInVars[$paramIndex] = $value; } } /** * In order to send lob data (clob/blob) to the Oracle data base, the * sqlToOracleBindVars function needs to have an ordered list of the * columns being addressed in the sql statement. * Since only insert and update statements require special handling, * there are two ways to find the columns: * 1) find the first set of () and parse out the columns names based on * the token ',' * 2) find all the text strings to the left of the equal signs. * * @param void * @return void */ private function setColumnArray() { $this->columns = array(); //handle the simple insert case first if(strtoupper(substr($this->sql, 0, 6)) == 'INSERT') { $firstPos = strpos($this->sql, '('); $secPos = strpos($this->sql, ')'); $collist = substr($this->sql, $firstPos + 1, $secPos - $firstPos - 1); $this->columns = explode(',', $collist); } if (strtoupper(substr($this->sql, 0, 6)) == 'UPDATE') { //handle more complex update case //first get the string setup so we can explode based on '=?' //second split results from previous action based on ' ' // the last token from this should be a column name $tmp = $this->sql; $tmp = str_replace(" =", "=", $this->sql); $tmp = str_replace("= ", "=", $tmp); $tmp = str_replace(",", " ", $tmp); $stage1 = explode("=?",$tmp); foreach($stage1 as $chunk) { $stage2 = explode(' ', $chunk); $this->columns[count($this->columns)] = $stage2[count($stage2) - 1]; } } } /** * @param int $paramIndex * @return void */ function setNull($paramIndex) { $this->boundInVars[$paramIndex] = ''; } }