<?php
/*
 *  $Id: StatementCommon.php,v 1.4 2004/06/13 02:31:07 hlellelid Exp $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information please see
 * <http://creole.phpdb.org>.
 */

/**
 * Class that contains common/shared functionality for Statements.
 * 
 * @author   Hans Lellelid <hans@xmpl.org>
 * @version  $Revision: 1.4 $
 * @package  creole.common
 */
abstract class StatementCommon {

    /**
     * The database connection.
     * @var Connection
     */ 
    protected $conn;
    
    /**
     * Temporarily hold a ResultSet object after an execute() query.
     * @var ResultSet
     */
    protected $resultSet;

    /**
     * Temporary hold the affected row cound after an execute() query.
     * @var int
     */
    protected $updateCount;
    
    /**
     * Array of warning objects generated by methods performed on result set.
     * @var array SQLWarning[]
     */
    protected $warnings = array();
    
    /** 
     * The ResultSet class name.
     * @var string
     */
    protected $resultClass;
    
    /**
     * The prepared statement resource id.
     * @var resource
     */
    protected $stmt;
    
    /**
     * Max rows to retrieve from DB.
     * @var int
     */
    protected $limit = 0;
    
    /**
     * Offset at which to start processing DB rows.
     * "Skip X rows"
     * @var int
     */
    protected $offset = 0;
    
    /**
     * Create new statement instance.
     * 
     * @param Connection $conn Connection object
     */ 
    function __construct(Connection $conn) 
    {
        $this->conn = $conn;        
    }
    
    /**
     * Sets the maximum number of rows to return from db.
     * This will affect the SQL if the RDBMS supports native LIMIT; if not,
     * it will be emulated.  Limit only applies to queries (not update sql).
     * @param int $v Maximum number of rows or 0 for all rows.
     * @return void
     */
    public function setLimit($v)
    {
        $this->limit = (int) $v;
    }
    
    /**
     * Returns the maximum number of rows to return or 0 for all.
     * @return int
     */
    public function getLimit()
    {
        return $this->limit;
    }
    
    /**
     * Sets the start row.
     * This will affect the SQL if the RDBMS supports native OFFSET; if not,
     * it will be emulated. Offset only applies to queries (not update) and 
     * only is evaluated when LIMIT is set!
     * @param int $v
     * @return void
     */ 
    public function setOffset($v)
    {
        $this->offset = (int) $v;
    }
    
    /**
     * Returns the start row.
     * Offset only applies when Limit is set!
     * @return int
     */
    public function getOffset()
    {
        return $this->offset;
    }
    
    /**
     * Free resources associated with this statement.
     * Some drivers will need to implement this method to free
     * database result resources. 
     * 
     * @return void
     */
    public function close()
    {
        // do nothing here (subclasses will implement)
    }
    
    /**
     * Generic execute() function has to check to see whether SQL is an update or select query.
     * 
     * If you already know whether it's a SELECT or an update (manipulating) SQL, then use
     * the appropriate method, as this one will incurr overhead to check the SQL.
     * 
     * @param int $fetchmode Fetchmode (only applies to queries).
     * @return boolean True if it is a result set, false if not or if no more results (this is identical to JDBC return val).
     * @throws SQLException
     * @todo -cStatementCommon Update execute() to not use isSelect() method, but rather to determine type based on returned results.
     */
    public function execute($sql, $fetchmode = null)
    {
        
        if (!$this->isSelect($sql)) {                    
            $this->updateCount = $this->executeUpdate($sql);
            return false;
        } else {
            $this->resultSet = $this->executeQuery($sql, $fetchmode);
            if ($this->resultSet->getRecordCount() === 0) {
                return false;
            }
            return true;
        }
    }

    /**
     * Get result set.
     * This assumes that the last thing done was an executeQuery() or an execute()
     * with SELECT-type query.
     *
     * @return RestultSet (or null if none)
     */
    public function getResultSet()
    {
        return $this->resultSet;
    }

    /**
     * Get update count.
     *
     * @return int Number of records affected, or <code>null</code> if not applicable.
     */
    public function getUpdateCount()
    {
        return $this->updateCount;
    }
        
    /**
     * Returns whether the passed SQL is a SELECT statement.
     * 
     * Returns true if SQL starts with 'SELECT' but not 'SELECT INTO'.  This exists
     * to support the execute() function -- which could either execute an update or
     * a query.
     * 
     * Currently this function does not take into consideration comments, primarily
     * because there are a number of different comment options for different drivers:
     * <pre>
     *     -- SQL-defined comment, but not truly comment in Oracle
     *  # comment in mysql
     *  /* comment in mssql, others * /
     *  // comment sometimes?
     *  REM also comment ...
     * </pre>
     * 
     * If you're wondering why we can't just execute the query and look at the return results
     * to see whether it was an update or a select, the reason is that for update queries we
     * need to do stuff before we execute them -- like start transactions if auto-commit is off.
     * 
     * @param string $sql
     * @return boolean Whether statement is a SELECT SQL statement.
     * @see execute()
     */
    protected function isSelect($sql)
    {
        // is first word is SELECT, then return true, unless it's SELECT INTO ...
        // this doesn't, however, take comments into account ...
        $sql = trim($sql);
        return (stripos($sql, 'select') === 0 && stripos($sql, 'select into ') !== 0);
    }

    /**
     * Executes the SQL query in this PreparedStatement object and returns the resultset generated by the query.
     * 
     * @param string $sql This method may optionally be called with the SQL statement.
     * @param int $fetchmode The mode to use when fetching the results (e.g. ResultSet::FETCHMODE_NUM, ResultSet::FETCHMODE_ASSOC).
     * @return object Creole::ResultSet
     * @throws SQLException If there is an error executing the specified query.
     * @todo -cStatementCommon Put native query execution logic in statement subclasses.
     */
    public function executeQuery($sql, $fetchmode = null)
    {
        $this->updateCount = null;
        if ($this->limit > 0 || $this->offset > 0) {
            $this->conn->applyLimit($sql, $this->offset, $this->limit);
        }
        $this->resultSet = $this->conn->executeQuery($sql, $fetchmode);
        return $this->resultSet;
    }

    /**
     * Executes the SQL INSERT, UPDATE, or DELETE statement in this PreparedStatement object.
     * 
     * @param string $sql This method may optionally be called with the SQL statement.
     * @return int Number of affected rows (or 0 for drivers that return nothing).
     * @throws SQLException if a database access error occurs.
     */
    public function executeUpdate($sql) 
    {
        if ($this->resultSet) $this->resultSet->close();
        $this->resultSet = null;
        $this->updateCount = $this->conn->executeUpdate($sql);
        return $this->updateCount;
    }
    
    /**
     * Gets next result set (if this behavior is supported by driver).
     * Some drivers (e.g. MSSQL) support returning multiple result sets -- e.g.
     * from stored procedures.
     *
     * This function also closes any current restult set.
     *
     * Default behavior is for this function to return false.  Driver-specific
     * implementations of this class can override this method if they actually
     * support multiple result sets.
     * 
     * @return boolean True if there is another result set, otherwise false.
     */
    public function getMoreResults()
    {
        if ($this->resultSet) $this->resultSet->close();        
        $this->resultSet = null;
        return false;        
    }
     
    /**
     * Gets the db Connection that created this statement.
     * @return Connection
     */
    public function getConnection()
    {
        return $this->conn;
    }
}
