Getting raw SQL query string from PDO prepared statements

前端 未结 16 858
心在旅途
心在旅途 2020-11-22 06:56

Is there a way to get the raw SQL string executed when calling PDOStatement::execute() on a prepared statement? For debugging purposes this would be extremely useful.

相关标签:
16条回答
  • 2020-11-22 07:02

    I assume you mean that you want the final SQL query, with parameter values interpolated into it. I understand that this would be useful for debugging, but it is not the way prepared statements work. Parameters are not combined with a prepared statement on the client-side, so PDO should never have access to the query string combined with its parameters.

    The SQL statement is sent to the database server when you do prepare(), and the parameters are sent separately when you do execute(). MySQL's general query log does show the final SQL with values interpolated after you execute(). Below is an excerpt from my general query log. I ran the queries from the mysql CLI, not from PDO, but the principle is the same.

    081016 16:51:28 2 Query       prepare s1 from 'select * from foo where i = ?'
                    2 Prepare     [2] select * from foo where i = ?
    081016 16:51:39 2 Query       set @a =1
    081016 16:51:47 2 Query       execute s1 using @a
                    2 Execute     [2] select * from foo where i = 1
    

    You can also get what you want if you set the PDO attribute PDO::ATTR_EMULATE_PREPARES. In this mode, PDO interpolate parameters into the SQL query and sends the whole query when you execute(). This is not a true prepared query. You will circumvent the benefits of prepared queries by interpolating variables into the SQL string before execute().


    Re comment from @afilina:

    No, the textual SQL query is not combined with the parameters during execution. So there's nothing for PDO to show you.

    Internally, if you use PDO::ATTR_EMULATE_PREPARES, PDO makes a copy of the SQL query and interpolates parameter values into it before doing the prepare and execute. But PDO does not expose this modified SQL query.

    The PDOStatement object has a property $queryString, but this is set only in the constructor for the PDOStatement, and it's not updated when the query is rewritten with parameters.

    It would be a reasonable feature request for PDO to ask them to expose the rewritten query. But even that wouldn't give you the "complete" query unless you use PDO::ATTR_EMULATE_PREPARES.

    This is why I show the workaround above of using the MySQL server's general query log, because in this case even a prepared query with parameter placeholders is rewritten on the server, with parameter values backfilled into the query string. But this is only done during logging, not during query execution.

    0 讨论(0)
  • 2020-11-22 07:02

    I need to log full query string after bind param so this is a piece in my code. Hope, it is useful for everyone hat has the same issue.

    /**
     * 
     * @param string $str
     * @return string
     */
    public function quote($str) {
        if (!is_array($str)) {
            return $this->pdo->quote($str);
        } else {
            $str = implode(',', array_map(function($v) {
                        return $this->quote($v);
                    }, $str));
    
            if (empty($str)) {
                return 'NULL';
            }
    
            return $str;
        }
    }
    
    /**
     * 
     * @param string $query
     * @param array $params
     * @return string
     * @throws Exception
     */
    public function interpolateQuery($query, $params) {
        $ps = preg_split("/'/is", $query);
        $pieces = [];
        $prev = null;
        foreach ($ps as $p) {
            $lastChar = substr($p, strlen($p) - 1);
    
            if ($lastChar != "\\") {
                if ($prev === null) {
                    $pieces[] = $p;
                } else {
                    $pieces[] = $prev . "'" . $p;
                    $prev = null;
                }
            } else {
                $prev .= ($prev === null ? '' : "'") . $p;
            }
        }
    
        $arr = [];
        $indexQuestionMark = -1;
        $matches = [];
    
        for ($i = 0; $i < count($pieces); $i++) {
            if ($i % 2 !== 0) {
                $arr[] = "'" . $pieces[$i] . "'";
            } else {
                $st = '';
                $s = $pieces[$i];
                while (!empty($s)) {
                    if (preg_match("/(\?|:[A-Z0-9_\-]+)/is", $s, $matches, PREG_OFFSET_CAPTURE)) {
                        $index = $matches[0][1];
                        $st .= substr($s, 0, $index);
                        $key = $matches[0][0];
                        $s = substr($s, $index + strlen($key));
    
                        if ($key == '?') {
                            $indexQuestionMark++;
                            if (array_key_exists($indexQuestionMark, $params)) {
                                $st .= $this->quote($params[$indexQuestionMark]);
                            } else {
                                throw new Exception('Wrong params in query at ' . $index);
                            }
                        } else {
                            if (array_key_exists($key, $params)) {
                                $st .= $this->quote($params[$key]);
                            } else {
                                throw new Exception('Wrong params in query with key ' . $key);
                            }
                        }
                    } else {
                        $st .= $s;
                        $s = null;
                    }
                }
                $arr[] = $st;
            }
        }
    
        return implode('', $arr);
    }
    
    0 讨论(0)
  • 2020-11-22 07:03

    I know this question is a bit old, but, I'm using this code since lot time ago (I've used response from @chris-go), and now, these code are obsolete with PHP 7.2

    I'll post an updated version of these code (Credit for the main code are from @bigwebguy, @mike and @chris-go, all of them answers of this question):

    /**
     * Replaces any parameter placeholders in a query with the value of that
     * parameter. Useful for debugging. Assumes anonymous parameters from 
     * $params are are in the same order as specified in $query
     *
     * @param string $query The sql query with parameter placeholders
     * @param array $params The array of substitution parameters
     * @return string The interpolated query
     */
    public function interpolateQuery($query, $params) {
        $keys = array();
        $values = $params;
    
        # build a regular expression for each parameter
        foreach ($params as $key => $value) {
            if (is_string($key)) {
                $keys[] = '/:'.$key.'/';
            } else {
                $keys[] = '/[?]/';
            }
    
            if (is_array($value))
                $values[$key] = implode(',', $value);
    
            if (is_null($value))
                $values[$key] = 'NULL';
        }
        // Walk the array to see if we can add single-quotes to strings
        array_walk($values, function(&$v, $k) { if (!is_numeric($v) && $v != "NULL") $v = "\'" . $v . "\'"; });
    
        $query = preg_replace($keys, $values, $query, 1, $count);
    
        return $query;
    }
    

    Note the change on the code are on array_walk() function, replacing create_function by an anonymous function. This make these good piece of code functional and compatible with PHP 7.2 (and hope future versions too).

    0 讨论(0)
  • 2020-11-22 07:03

    Mike's answer is working good until you are using the "re-use" bind value.
    For example:

    SELECT * FROM `an_modules` AS `m` LEFT JOIN `an_module_sites` AS `ms` ON m.module_id = ms.module_id WHERE 1 AND `module_enable` = :module_enable AND `site_id` = :site_id AND (`module_system_name` LIKE :search OR `module_version` LIKE :search)
    

    The Mike's answer can only replace first :search but not the second.
    So, I rewrite his answer to work with multiple parameters that can re-used properly.

    public function interpolateQuery($query, $params) {
        $keys = array();
        $values = $params;
        $values_limit = [];
    
        $words_repeated = array_count_values(str_word_count($query, 1, ':_'));
    
        # build a regular expression for each parameter
        foreach ($params as $key => $value) {
            if (is_string($key)) {
                $keys[] = '/:'.$key.'/';
                $values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);
            } else {
                $keys[] = '/[?]/';
                $values_limit = [];
            }
    
            if (is_string($value))
                $values[$key] = "'" . $value . "'";
    
            if (is_array($value))
                $values[$key] = "'" . implode("','", $value) . "'";
    
            if (is_null($value))
                $values[$key] = 'NULL';
        }
    
        if (is_array($values)) {
            foreach ($values as $key => $val) {
                if (isset($values_limit[$key])) {
                    $query = preg_replace(['/:'.$key.'/'], [$val], $query, $values_limit[$key], $count);
                } else {
                    $query = preg_replace(['/:'.$key.'/'], [$val], $query, 1, $count);
                }
            }
            unset($key, $val);
        } else {
            $query = preg_replace($keys, $values, $query, 1, $count);
        }
        unset($keys, $values, $values_limit, $words_repeated);
    
        return $query;
    }
    
    0 讨论(0)
  • 2020-11-22 07:04

    A solution is to voluntarily put an error in the query and to print the error's message:

    //Connection to the database
    $co = new PDO('mysql:dbname=myDB;host=localhost','root','');
    //We allow to print the errors whenever there is one
    $co->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    //We create our prepared statement
    $stmt = $co->prepare("ELECT * FROM Person WHERE age=:age"); //I removed the 'S' of 'SELECT'
    $stmt->bindValue(':age','18',PDO::PARAM_STR);
    try {
        $stmt->execute();
    } catch (PDOException $e) {
        echo $e->getMessage();
    }
    

    Standard output:

    SQLSTATE[42000]: Syntax error or access violation: [...] near 'ELECT * FROM Person WHERE age=18' at line 1

    It is important to note that it only prints the first 80 characters of the query.

    0 讨论(0)
  • 2020-11-22 07:06

    I modified the method to include handling output of arrays for statements like WHERE IN (?).

    UPDATE: Just added check for NULL value and duplicated $params so actual $param values are not modified.

    Great work bigwebguy and thanks!

    /**
     * Replaces any parameter placeholders in a query with the value of that
     * parameter. Useful for debugging. Assumes anonymous parameters from 
     * $params are are in the same order as specified in $query
     *
     * @param string $query The sql query with parameter placeholders
     * @param array $params The array of substitution parameters
     * @return string The interpolated query
     */
    public function interpolateQuery($query, $params) {
        $keys = array();
        $values = $params;
    
        # build a regular expression for each parameter
        foreach ($params as $key => $value) {
            if (is_string($key)) {
                $keys[] = '/:'.$key.'/';
            } else {
                $keys[] = '/[?]/';
            }
    
            if (is_string($value))
                $values[$key] = "'" . $value . "'";
    
            if (is_array($value))
                $values[$key] = "'" . implode("','", $value) . "'";
    
            if (is_null($value))
                $values[$key] = 'NULL';
        }
    
        $query = preg_replace($keys, $values, $query);
    
        return $query;
    }
    
    0 讨论(0)
提交回复
热议问题