Zend Framework

Solution for running non-prepared SQL statements

Details

Description

The default implementation of the query() method in Zend_Db_Adapter_Abstract performs a prepare() on every SQL statement.

Some SQL statements are incompatible with prepare(). This varies by RDBMS brand and client API design, but there are usually some such statements.

For example, MySQL does not support preparing DDL or SHOW statements.

This is a feature request to add some method to Zend_Db_Adapter_Abstract to execute a SQL statement without performing a prepare(). In the case of PDO adapters, this is the exec() method of the underlying PDO object. But other database extensions have different methods to achieve this goal.

Issue Links

Activity

Hide
Bill Karwin added a comment -

Non-prepared statements are also important for MySQL because the MySQL Query Cache feature cannot cache prepared statements.

Show
Bill Karwin added a comment - Non-prepared statements are also important for MySQL because the MySQL Query Cache feature cannot cache prepared statements.
Hide
julien PAULI added a comment -

Yeah, just read that please : http://netevil.org/node.php?uuid=444a6017-0548-2459-2943-44a601714d58

I don't know if that bug has already been reported, but i met it on RC1.0 under a linux server.
The query was

$this->db->query("ALTER TABLE `......` ADD( '..............')");

using PDO under Mysql(4.1) and Linux throws an exception 'SQLSTATE[HY000]: General error: 2030 This command is not supported in
the prepared statement protocol yet' in {file}

The only way to fix it was to modify the Zend/Db/Adapter/PdoAbstract.php by adding

$this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

There is no way to change that behavior other than modify it manually in the framework source. And as said here : there is no was to prevent adapter from using prepared statements...

Show
julien PAULI added a comment - Yeah, just read that please : http://netevil.org/node.php?uuid=444a6017-0548-2459-2943-44a601714d58 I don't know if that bug has already been reported, but i met it on RC1.0 under a linux server. The query was
$this->db->query("ALTER TABLE `......` ADD( '..............')");
using PDO under Mysql(4.1) and Linux throws an exception 'SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yet' in {file} The only way to fix it was to modify the Zend/Db/Adapter/PdoAbstract.php by adding
$this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
There is no way to change that behavior other than modify it manually in the framework source. And as said here : there is no was to prevent adapter from using prepared statements...
Hide
Darby Felton added a comment -
Show
Darby Felton added a comment - Related to Bill's comment: http://fr3.php.net/manual/en/function.PDO-prepare.php#79178
Hide
Wil Sinclair added a comment -

Please categorize/fix as needed.

Show
Wil Sinclair added a comment - Please categorize/fix as needed.
Hide
Till Klampaeckel added a comment -

Of course I only found this issue after opening another one. Can someone mark ZF-4767 as duplicate. Apologies for the extra work.

Also, what's the status of this issue? I'd like to see it fixed soon.

Show
Till Klampaeckel added a comment - Of course I only found this issue after opening another one. Can someone mark ZF-4767 as duplicate. Apologies for the extra work. Also, what's the status of this issue? I'd like to see it fixed soon.
Hide
Till Klampaeckel added a comment -

Adding the link, to stress the importance of the issue:
http://www.nabble.com/Zend_DB-performance-issue-td14233508.html

Show
Till Klampaeckel added a comment - Adding the link, to stress the importance of the issue: http://www.nabble.com/Zend_DB-performance-issue-td14233508.html
Hide
Ralph Schindler added a comment -

I will evaluate this with the next 2 weeks.

Show
Ralph Schindler added a comment - I will evaluate this with the next 2 weeks.
Hide
Till Klampaeckel added a comment - - edited

This is still a blocker for me.

Below is some code which I started using in some of my projects to use unprepared queries and to utilize the query cache to a certain extend. The __call() is a way to debug this and to find methods I didn't cover yet.

This is not final but a work in progress and an idea. I could extend it, etc. if I get some feedback here.

Zend_Db_Statement_Mysqli_Unprepared
<?php
/**
 * @see Zend_Db_Statement
 */
require_once 'Zend/Db/Statement.php';

class Zend_Db_Statement_Mysqli_Unprepared extends Zend_Db_Statement
{
    protected $_fetchMode, $_result;

    public function __construct($result, $fetchMode)
    {
        $this->_result    = $result;
        $this->_fetchMode = $fetchMode;
    }

    public function fetchAll($fetchMode = null)
    {
        $data = array();
        while ($row = $this->fetch($fetchMode)) {
            array_push($data, $row);
        }
        return $data;
    }

    public function fetchColumn($col = 0)
    {
        $row = $this->fetch(Zend_Db::FETCH_NUM);
        if (isset($row[$col])) {
            return $row[$col];
        }
        return false; // FIXME
    }

    public function fetch($fetchMode = null)
    {
        if ($fetchMode === null) {
            $fetchMode = $this->_fetchMode;
        }

        switch ($fetchMode) {
            default:
            case Zend_Db::FETCH_BOTH:
                $fetchMode = MYSQLI_BOTH;
                break;

            case Zend_Db::FETCH_ASSOC:
                $fetchMode = MYSQLI_ASSOC;
                break;

            case Zend_Db::FETCH_NUM:
                $fetchMode = MYSQLI_NUM;
                break;
        }

        return $this->_result->fetch_array($fetchMode);
    }

    public function __call($method, $args)
    {
        die("$method");
    }
}

Here is a patch to Zend_Db_Adapter_Abstract:

Zend_Db_Adapter_Abstract.diff
Index: library/Zend/Db/Adapter/Abstract.php
===================================================================
--- library/Zend/Db/Adapter/Abstract.php	(revision 12330)
+++ library/Zend/Db/Adapter/Abstract.php	(working copy)
@@ -132,6 +132,12 @@
     );
 
     /**
+     * @var boolean Defines whether the adapter uses prepared statemtents.
+     *              The default is 'true' to avoid a BC-break.
+     */
+    protected $_usePreparedStatements = true;
+
+    /**
      * Constructor.
      *
      * $config is an array of key/value pairs or an instance of Zend_Config
@@ -406,7 +412,7 @@
      * @param  mixed  $sql  The SQL statement with placeholders.
      *                      May be a string or Zend_Db_Select.
      * @param  mixed  $bind An array of data to bind to the placeholders.
-     * @return Zend_Db_Statement_Interface
+     * @return mixed  Zend_Db_Statement_Interface or query result.
      */
     public function query($sql, $bind = array())
     {
@@ -425,13 +431,26 @@
             $bind = array($bind);
         }
 
-        // prepare and execute the statement with profiling
-        $stmt = $this->prepare($sql);
-        $stmt->execute($bind);
+        if ($this->_usePreparedStatements === true) {
+            // prepare and execute the statement with profiling
+            $stmt = $this->prepare($sql);
+            $stmt->execute($bind);
 
-        // return the results embedded in the prepared statement object
-        $stmt->setFetchMode($this->_fetchMode);
-        return $stmt;
+            // return the results embedded in the prepared statement object
+            $stmt->setFetchMode($this->_fetchMode);
+            return $stmt;
+        }
+
+        if (!empty($bind)) {
+            throw new Zend_Db_Exception('You can\'t disable prepared statements, yet supply parameters to bind.');
+        }
+
+        $fetchClass = get_class($this) . '_Fetch';
+        Zend_Loader::loadClass($fetchClass);
+        return new $fetchClass(
+            $this->getConnection()->query($sql),
+            $this->_fetchMode
+        );
     }
 
     /**
@@ -1022,6 +1041,24 @@
     }
 
     /**
+     * Enable or disable prepared statements here.
+     *
+     * @param bool $use 'true' or 'false'.
+     *
+     * @return self
+     * @uses   self::$_usePreparedStatements
+     */
+    public function usePrepared($use)
+    {
+        if ($use === true) {
+            $this->_usePreparedStatements = $use;
+            return $this;
+        }
+        $this->_usePreparedStatements = false;
+        return $this;
+    }
+
+    /**
      * Abstract Methods
      */

And here is some code demonstrating usage:

example
<?php
set_include_path('.:' . dirname(__FILE__) . '/trunk/library');

require_once 'Zend/Loader.php';
require_once 'Zend/Db.php';
require_once 'Zend/Db/Select.php';

try {
    $db = Zend_Db::factory(
        'Mysqli',
        array(
            'username' => 'root',
            'password' => '',
            'host'     => 'localhost',
            'dbname'   => 'mysql'
        )
    );
    $db->usePrepared(true);

    $select = $db->select();
    $select->from('user', array('Host', 'User'));

    var_dump($db->fetchAssoc($select));


    $db->usePrepared(false);

    $select->reset();
    $select = $db->select();
    $select->from('user', array('Host', 'User'));

    var_dump($db->fetchAssoc($select));

} catch (Exception $e) {
    die($e->getMessage());
}

I briefly ran tests and it didn't break anything, but no guarantees. I also cloned the tests for the Mysqli driver and ran them with usePrepared() and it worked. This is just an idea.

The usePrepared() call is more or less an idea to change the behaviour on run time. I also envision a config option which you can pass in with a Zend_Config object or an array.

I think by default this should be disabled but that would break BC so I guess I could live with it the other way around too. Haven't made up my mind up though, I just needed to get rid off prepared queries.

Show
Till Klampaeckel added a comment - - edited This is still a blocker for me. Below is some code which I started using in some of my projects to use unprepared queries and to utilize the query cache to a certain extend. The __call() is a way to debug this and to find methods I didn't cover yet. This is not final but a work in progress and an idea. I could extend it, etc. if I get some feedback here.
Zend_Db_Statement_Mysqli_Unprepared
<?php
/**
 * @see Zend_Db_Statement
 */
require_once 'Zend/Db/Statement.php';

class Zend_Db_Statement_Mysqli_Unprepared extends Zend_Db_Statement
{
    protected $_fetchMode, $_result;

    public function __construct($result, $fetchMode)
    {
        $this->_result    = $result;
        $this->_fetchMode = $fetchMode;
    }

    public function fetchAll($fetchMode = null)
    {
        $data = array();
        while ($row = $this->fetch($fetchMode)) {
            array_push($data, $row);
        }
        return $data;
    }

    public function fetchColumn($col = 0)
    {
        $row = $this->fetch(Zend_Db::FETCH_NUM);
        if (isset($row[$col])) {
            return $row[$col];
        }
        return false; // FIXME
    }

    public function fetch($fetchMode = null)
    {
        if ($fetchMode === null) {
            $fetchMode = $this->_fetchMode;
        }

        switch ($fetchMode) {
            default:
            case Zend_Db::FETCH_BOTH:
                $fetchMode = MYSQLI_BOTH;
                break;

            case Zend_Db::FETCH_ASSOC:
                $fetchMode = MYSQLI_ASSOC;
                break;

            case Zend_Db::FETCH_NUM:
                $fetchMode = MYSQLI_NUM;
                break;
        }

        return $this->_result->fetch_array($fetchMode);
    }

    public function __call($method, $args)
    {
        die("$method");
    }
}
Here is a patch to Zend_Db_Adapter_Abstract:
Zend_Db_Adapter_Abstract.diff
Index: library/Zend/Db/Adapter/Abstract.php
===================================================================
--- library/Zend/Db/Adapter/Abstract.php	(revision 12330)
+++ library/Zend/Db/Adapter/Abstract.php	(working copy)
@@ -132,6 +132,12 @@
     );
 
     /**
+     * @var boolean Defines whether the adapter uses prepared statemtents.
+     *              The default is 'true' to avoid a BC-break.
+     */
+    protected $_usePreparedStatements = true;
+
+    /**
      * Constructor.
      *
      * $config is an array of key/value pairs or an instance of Zend_Config
@@ -406,7 +412,7 @@
      * @param  mixed  $sql  The SQL statement with placeholders.
      *                      May be a string or Zend_Db_Select.
      * @param  mixed  $bind An array of data to bind to the placeholders.
-     * @return Zend_Db_Statement_Interface
+     * @return mixed  Zend_Db_Statement_Interface or query result.
      */
     public function query($sql, $bind = array())
     {
@@ -425,13 +431,26 @@
             $bind = array($bind);
         }
 
-        // prepare and execute the statement with profiling
-        $stmt = $this->prepare($sql);
-        $stmt->execute($bind);
+        if ($this->_usePreparedStatements === true) {
+            // prepare and execute the statement with profiling
+            $stmt = $this->prepare($sql);
+            $stmt->execute($bind);
 
-        // return the results embedded in the prepared statement object
-        $stmt->setFetchMode($this->_fetchMode);
-        return $stmt;
+            // return the results embedded in the prepared statement object
+            $stmt->setFetchMode($this->_fetchMode);
+            return $stmt;
+        }
+
+        if (!empty($bind)) {
+            throw new Zend_Db_Exception('You can\'t disable prepared statements, yet supply parameters to bind.');
+        }
+
+        $fetchClass = get_class($this) . '_Fetch';
+        Zend_Loader::loadClass($fetchClass);
+        return new $fetchClass(
+            $this->getConnection()->query($sql),
+            $this->_fetchMode
+        );
     }
 
     /**
@@ -1022,6 +1041,24 @@
     }
 
     /**
+     * Enable or disable prepared statements here.
+     *
+     * @param bool $use 'true' or 'false'.
+     *
+     * @return self
+     * @uses   self::$_usePreparedStatements
+     */
+    public function usePrepared($use)
+    {
+        if ($use === true) {
+            $this->_usePreparedStatements = $use;
+            return $this;
+        }
+        $this->_usePreparedStatements = false;
+        return $this;
+    }
+
+    /**
      * Abstract Methods
      */
And here is some code demonstrating usage:
example
<?php
set_include_path('.:' . dirname(__FILE__) . '/trunk/library');

require_once 'Zend/Loader.php';
require_once 'Zend/Db.php';
require_once 'Zend/Db/Select.php';

try {
    $db = Zend_Db::factory(
        'Mysqli',
        array(
            'username' => 'root',
            'password' => '',
            'host'     => 'localhost',
            'dbname'   => 'mysql'
        )
    );
    $db->usePrepared(true);

    $select = $db->select();
    $select->from('user', array('Host', 'User'));

    var_dump($db->fetchAssoc($select));


    $db->usePrepared(false);

    $select->reset();
    $select = $db->select();
    $select->from('user', array('Host', 'User'));

    var_dump($db->fetchAssoc($select));

} catch (Exception $e) {
    die($e->getMessage());
}
I briefly ran tests and it didn't break anything, but no guarantees. I also cloned the tests for the Mysqli driver and ran them with usePrepared() and it worked. This is just an idea. The usePrepared() call is more or less an idea to change the behaviour on run time. I also envision a config option which you can pass in with a Zend_Config object or an array. I think by default this should be disabled but that would break BC so I guess I could live with it the other way around too. Haven't made up my mind up though, I just needed to get rid off prepared queries.
Hide
Benjamin Eberlei added a comment -

The fix looks good, by default i would enable the preparing of statements and allow to pass a config value to disable it.

Show
Benjamin Eberlei added a comment - The fix looks good, by default i would enable the preparing of statements and allow to pass a config value to disable it.
Hide
Ralph Schindler added a comment -

Why is there the requirement that non-preparable queries must go through query?

Why not add a method called execute() ?
OR- why not simply use the Statement? or the getConnection()?

Show
Ralph Schindler added a comment - Why is there the requirement that non-preparable queries must go through query? Why not add a method called execute() ? OR- why not simply use the Statement? or the getConnection()?
Hide
Josh Butts added a comment -

I agree that this should be possible to do, but don't necessary agree with the API implementation outlined in the patch above

Show
Josh Butts added a comment - I agree that this should be possible to do, but don't necessary agree with the API implementation outlined in the patch above
Hide
Pieter Kokx added a comment -

I attached a patch for this issue. It does only contain a fix for PDO adapters yet, other adapters will fail because there is an abstract function missing.

Show
Pieter Kokx added a comment - I attached a patch for this issue. It does only contain a fix for PDO adapters yet, other adapters will fail because there is an abstract function missing.
Hide
Adam Lundrigan added a comment -

This issue is specifically mentioned in the Zend\Db RFC for ZFv2. However, is this something which should be fixed in ZFv1 as well?

Show
Adam Lundrigan added a comment - This issue is specifically mentioned in the Zend\Db RFC for ZFv2. However, is this something which should be fixed in ZFv1 as well?

People

Vote (43)
Watch (22)

Dates

  • Created:
    Updated:

Time Tracking

Estimated:
2w
Original Estimate - 2 weeks
Remaining:
2w
Remaining Estimate - 2 weeks
Logged:
Not Specified
Time Spent - Not Specified