147

Before moving to PDO, I created SQL queries in PHP by concatenating strings. If I got database syntax error, I could just echo the final SQL query string, try it myself on the database, and tweak it until I fixed the error, then put that back into the code.

Prepared PDO statements are faster and better and safer, but one thing bothers me: I never see the final query as it's sent to the database. When I get errors about the syntax in my Apache log or my custom log file (I log errors inside a catch block), I can't see the query that caused them.

Is there a way capture the complete SQL query sent by PDO to the database and log it to a file?

Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 4
    It **is** logged in a file: `/var/log/mysql/*`. PDO bound parameters cannot cause syntax errors so all you need is the prepared SQL query. – Xeoncross Oct 10 '11 at 18:10
  • 1
    see the code in http://stackoverflow.com/questions/210564/getting-raw-sql-query-string-from-pdo-prepared-statements (not in the accepted answer). Not that there have been a few updates posted. – Mawg says reinstate Monica Jun 03 '14 at 09:25
  • 1
    Simple one-line via Composer: https://github.com/panique/pdo-debug – Sliq Jul 09 '14 at 19:37
  • 2
    Xeoncross's answer helped me. Here is an article explaining how to turn this feature on. It's off be default on a lot of server installations. http://www.pontikis.net/blog/how-and-when-to-enable-mysql-logs – mrbinky3000 Oct 16 '14 at 14:23
  • 2
    Try with `var_dump($pdo_instance->debugDumpParams())` – Daniel Petrovaliev Jul 09 '15 at 09:11

18 Answers18

101

You say this :

I never see the final query as it's sent to the database

Well, actually, when using prepared statements, there is no such thing as a "final query" :

  • First, a statement is sent to the DB, and prepared there
    • The database parses the query, and builds an internal representation of it
  • And, when you bind variables and execute the statement, only the variables are sent to the database
    • And the database "injects" the values into its internal representation of the statement


So, to answer your question :

Is there a way capture the complete SQL query sent by PDO to the database and log it to a file?

No : as there is no "complete SQL query" anywhere, there is no way to capture it.


The best thing you can do, for debugging purposes, is "re-construct" an "real" SQL query, by injecting the values into the SQL string of the statement.

What I usually do, in this kind of situations, is :

  • echo the SQL code that corresponds to the statement, with placeholders
  • and use var_dump (or an equivalent) just after, to display the values of the parameters
  • This is generally enough to see a possible error, even if you don't have any "real" query that you can execute.

This is not great, when it comes to debugging -- but that's the price of prepared statements and the advantages they bring.

Pascal MARTIN
  • 395,085
  • 80
  • 655
  • 663
  • 1
    Great explanation - thanks. Apparently I had only fuzzy ideas of how this works. I suppose when the statement is prepared, the resulting object contains a hash or numerical ID that can be sent back to the database with the parameters to plug in. – Nathan Long Mar 09 '10 at 18:01
  • You're welcome :-) ;;; I doon't know how this is implemented in details, but I suppose it's something like that -- the result is exactly like that, anyway ;;; that's one of the nice things with prepared statements : if you have to execute the same query a lot of times, it'll only be sent to the DB and prepared once : for each execution, only the data will be sent. – Pascal MARTIN Mar 09 '10 at 18:06
  • 1
    Update: Aaron Patterson mentioned at Railsconf 2011 that he added more prepared statements to Rails, but that the benefit is much heavier in PostgreSQL than in MySQL. He said that this is because MySQL doesn't actually create the query plan until you execute the prepared query. – Nathan Long Jul 20 '11 at 13:46
87

Looking in the database log

Although Pascal MARTIN is correct that PDO doesn't send the complete query to the database all at once, ryeguy's suggestion to use the DB's logging function actually allowed me to see the complete query as assembled and executed by the database.

Here's how: (These instructions are for MySQL on a Windows machine - your mileage may vary)

  • In my.ini, under the [mysqld] section, add a log command, like log="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log"
  • Restart MySQL.
  • It will start logging every query in that file.

That file will grow quickly, so be sure to delete it and turn off logging when you're done testing.

Community
  • 1
  • 1
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 1
    Just a note--I had to escape the slashes in my.ini. So, my entry looked something like log="C:\\temp\\MySQL\\mysql.log". – Jim Oct 18 '11 at 23:45
  • Just add `log` by itself under `[mysqld]`. It will log to `...\data\.log`. – rustyx Feb 01 '12 at 15:40
  • 4
    This *may* work depending on the setting of `PDO::ATTR_EMULATE_PREPARES`. See this answer for more info: http://stackoverflow.com/questions/10658865/#answer-10658929 – webbiedave May 18 '12 at 20:05
  • 29
    I hate PDO because of this. – Salman Feb 11 '13 at 14:38
  • 1
    @webbiedave - oh, wow! Your linked answer implies that my answer only works when PDO is not working optimally, but rather sending the whole query for backwards compatibility with old version of MySQL or an old driver. Interesting. – Nathan Long Feb 21 '13 at 10:28
  • 14
    In MySQL 5.5+ you need `general_log` instead of `log`. See http://dev.mysql.com/doc/refman/5.5/en/query-log.html – Adrian Macneil Sep 07 '13 at 05:54
  • 1
    And for MAC users, where can we find this file please ? – user3553866 Apr 13 '15 at 13:58
  • 1
    For Linux/Mac users check file /etc/mysql/mysql.conf.d/mysqld.conf and un comment line with general_log parameter. – dikirill Nov 19 '15 at 02:27
  • 1
    Here are directions for Ubuntu users: https://stackoverflow.com/a/47420858/3965565 Thanks for the inspiration @dikirill – elbowlobstercowstand Nov 21 '17 at 19:17
  • Not working for me. Im getting [ERROR] unknown variable log="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log" – Adrian Danlos Nov 25 '19 at 15:37
  • For me, even though general log is enabled and working, it does not shows the failed queries, but only the working ones. Also the mysql error log doesn't show anything. I can see the mysql error message from the exception raised in php only. – Vitomakes Nov 25 '20 at 14:17
  • For newer version of MariaDB, MySQL or if you got an error **ambiguous option --log**, Please use `general_log general_log_file="/path/to/mysql.log"`. Read more at https://mariadb.com/kb/en/general-query-log/ – vee Mar 24 '21 at 06:59
22

Sure you can debug using this mode {{ PDO::ATTR_ERRMODE }} Just add new line before your query then you will show the debug lines.

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$db->query('SELECT *******');  
j0k
  • 22,600
  • 28
  • 79
  • 90
Saud Alfadhli
  • 846
  • 1
  • 8
  • 11
21

Probably what you want to do is use debugDumpParams() on the statement handle. You can run that any time after binding values to the prepared query (no need to execute() the statement).

It doesn't build the prepared statement for you, but it will show your parameters.

afeique
  • 395
  • 6
  • 15
fijiaaron
  • 5,015
  • 3
  • 35
  • 28
12

An old post but perhaps someone will find this useful;

function pdo_sql_debug($sql,$placeholders){
    foreach($placeholders as $k => $v){
        $sql = preg_replace('/:'.$k.'/',"'".$v."'",$sql);
    }
    return $sql;
}
dontaskagain
  • 251
  • 1
  • 3
  • 7
  • 2
    For a similar function that can also handle numeric parameters, see [my answer](http://stackoverflow.com/a/19326169/560114) (thanks to a commenter on php.net). – Matt Browne Oct 11 '13 at 19:54
10

Here's a function to see what the effective SQL will be, adpated from a comment by "Mark" at php.net:

function sql_debug($sql_string, array $params = null) {
    if (!empty($params)) {
        $indexed = $params == array_values($params);
        foreach($params as $k=>$v) {
            if (is_object($v)) {
                if ($v instanceof \DateTime) $v = $v->format('Y-m-d H:i:s');
                else continue;
            }
            elseif (is_string($v)) $v="'$v'";
            elseif ($v === null) $v='NULL';
            elseif (is_array($v)) $v = implode(',', $v);

            if ($indexed) {
                $sql_string = preg_replace('/\?/', $v, $sql_string, 1);
            }
            else {
                if ($k[0] != ':') $k = ':'.$k; //add leading colon if it was left out
                $sql_string = str_replace($k,$v,$sql_string);
            }
        }
    }
    return $sql_string;
}
Matt Browne
  • 12,169
  • 4
  • 59
  • 75
  • Why does "Mark" use colon before $k in `str_replace(":$k" ....`? Associative indexes already have it in the $params array. – Alan Mar 29 '15 at 22:12
  • Good question...this might explain it: http://stackoverflow.com/questions/9778887/is-the-leading-colon-for-parameter-names-passed-to-pdostatementbindparam-opt. Personally I used this function to debug Doctrine queries, and I think Doctrine uses numbered rather than named parameters so I didn't notice this issue. I updated the function so that it will work either with or without leading colons now. – Matt Browne Mar 30 '15 at 13:28
  • note, that this solution replaces `:name_long` with `:name`. At least if `:name` comes before `:name_long`. MySQL prepared statements can handle this correctly, so don't let that confuse you. – Zim84 Aug 25 '18 at 17:44
8

No. PDO queries are not prepared on the client side. PDO simply sends the SQL query and the parameters to the database server. The database is what does the substitution (of the ?'s). You have two options:

  • Use your DB's logging function (but even then it's normally shown as two separate statements (ie, "not final") at least with Postgres)
  • Output the SQL query and the paramaters and piece it together yourself
ryeguy
  • 65,519
  • 58
  • 198
  • 260
  • I never thought to check the DB's log. I'm poking around in the MySQL directory and don't see any log files, but maybe logging is an option I have to turn on somewhere. – Nathan Long Mar 09 '10 at 18:06
  • Yes, you have to turn it on. I don't know the specifics but by default it does not log every query. – ryeguy Mar 09 '10 at 18:16
6

almost nothing was said about error displaying except check error logs, but there's a rather helpful functionality:

<?php
/* Provoke an error -- bogus SQL syntax */
$stmt = $dbh->prepare('bogus sql');
if (!$stmt) {
    echo "\PDO::errorInfo():\n";
    print_r($dbh->errorInfo());
}
?>

(source link)

it is clear that this code can be modified to be used as exception message or any other kind of error handling

Zippp
  • 448
  • 6
  • 17
  • 2
    This is wrong way. PDO is smart enough to make this code useless. Just tell it to throw exceptions on errors. PHP will do the rest, **way better** than this limited function. Also, **please**, learn not to print all the errors directly into browser. There are better ways. – Your Common Sense Mar 26 '13 at 05:41
  • 3
    that's the official documentation, and of course no one was going to print that error in production, again this is an example from the official site (php.net), see the link below the code example. And for sure much better is to use additional params $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) within the PDO instantiation but unfortunately you could not have access to that code – Zippp Mar 28 '13 at 14:52
5

for example you have this pdo statement :

$query="insert into tblTest (field1, field2, field3)
values (:val1, :val2, :val3)";
$res=$db->prepare($query);
$res->execute(array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
));

now you can get the executed query by defining an array like this :

$assoc=array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
);
$exQuery=str_replace(array_keys($assoc), array_values($assoc), $query);
echo $exQuery;
Alireza
  • 884
  • 9
  • 17
3

Searching internet I found this as an acceptable solution. A different class is used instead of PDO and PDO functions are called through magic function calls. I am not sure this creates serious performance problems. But it can be used until a sensible logging feature is added to PDO.

So as per this thread, you can write a wrapper for your PDO connection which can log and throws an exception when you get a error.

Here is simple example:

class LoggedPDOSTatement extends PDOStatement    {

function execute ($array)    {
    parent::execute ($array);
    $errors = parent::errorInfo();
    if ($errors[0] != '00000'):
        throw new Exception ($errors[2]);
    endif;
  }

}

so you can use that class instead of PDOStatement:

$this->db->setAttribute (PDO::ATTR_STATEMENT_CLASS, array ('LoggedPDOStatement', array()));

Here a mentioned PDO decorator implementation:

class LoggedPDOStatement    {

function __construct ($stmt)    {
    $this->stmt = $stmt;
}

function execute ($params = null)    {
    $result = $this->stmt->execute ($params); 
    if ($this->stmt->errorCode() != PDO::ERR_NONE):
        $errors = $this->stmt->errorInfo();
        $this->paint ($errors[2]);
    endif;
    return $result;
}

function bindValue ($key, $value)    {
    $this->values[$key] = $value;    
    return $this->stmt->bindValue ($key, $value);
}

function paint ($message = false)    {
    echo '<pre>';
    echo '<table cellpadding="5px">';
    echo '<tr><td colspan="2">Message: ' . $message . '</td></tr>';
    echo '<tr><td colspan="2">Query: ' . $this->stmt->queryString . '</td></tr>';
    if (count ($this->values) > 0):
    foreach ($this->values as $key => $value):
    echo '<tr><th align="left" style="background-color: #ccc;">' . $key . '</th><td>' . $value . '</td></tr>';
    endforeach;
    endif;
    echo '</table>';
    echo '</pre>';
}

function __call ($method, $params)    {
    return call_user_func_array (array ($this->stmt, $method), $params); 
}

}
kenorb
  • 155,785
  • 88
  • 678
  • 743
bkilinc
  • 989
  • 2
  • 13
  • 28
2

To log MySQL in WAMP, you will need to edit the my.ini (e.g. under wamp\bin\mysql\mysql5.6.17\my.ini)

and add to [mysqld]:

general_log = 1
general_log_file="c:\\tmp\\mysql.log"
Spezi
  • 21
  • 2
2

Here is a function I made to return a SQL query with "resolved" parameters.

function paramToString($query, $parameters) {
    if(!empty($parameters)) {
        foreach($parameters as $key => $value) {
            preg_match('/(\?(?!=))/i', $query, $match, PREG_OFFSET_CAPTURE);
            $query = substr_replace($query, $value, $match[0][1], 1);
        }
    }
    return $query;
    $query = "SELECT email FROM table WHERE id = ? AND username = ?";
    $values = [1, 'Super'];

    echo paramToString($query, $values);

Assuming you execute like this

$values = array(1, 'SomeUsername');
$smth->execute($values);

This function DOES NOT add quotes to queries but does the job for me.

yemd
  • 420
  • 4
  • 15
1

I've created a modern Composer-loaded project / repository for exactly this here:

pdo-debug

Find the project's GitHub home here, see a blog post explaining it here. One line to add in your composer.json, and then you can use it like this:

echo debugPDO($sql, $parameters);

$sql is the raw SQL statement, $parameters is an array of your parameters: The key is the placeholder name (":user_id") or the number of the unnamed parameter ("?"), the value is .. well, the value.

The logic behind: This script will simply grad the parameters and replace them into the SQL string provided. Super-simple, but super-effective for 99% of your use-cases. Note: This is just a basic emulation, not a real PDO debugging (as this is not possible as PHP sends raw SQL and parameters to the MySQL server seperated).

A big thanks to bigwebguy and Mike from the StackOverflow thread Getting raw SQL query string from PDO for writing basically the entire main function behind this script. Big up!

Community
  • 1
  • 1
Sliq
  • 15,937
  • 27
  • 110
  • 143
1

How to debug PDO mysql database queries in Ubuntu

TL;DR Log all your queries and tail the mysql log.

These directions are for my install of Ubuntu 14.04. Issue command lsb_release -a to get your version. Your install might be different.

Turn on logging in mysql

  1. Go to your dev server cmd line
  2. Change directories cd /etc/mysql. You should see a file called my.cnf. That’s the file we’re gonna change.
  3. Verify you’re in the right place by typing cat my.cnf | grep general_log. This filters the my.cnf file for you. You should see two entries: #general_log_file = /var/log/mysql/mysql.log && #general_log = 1.
  4. Uncomment those two lines and save via your editor of choice.
  5. Restart mysql: sudo service mysql restart.
  6. You might need to restart your webserver too. (I can’t recall the sequence I used). For my install, that’s nginx: sudo service nginx restart.

Nice work! You’re all set. Now all you have to do is tail the log file so you can see the PDO queries your app makes in real time.

Tail the log to see your queries

Enter this cmd tail -f /var/log/mysql/mysql.log.

Your output will look something like this:

73 Connect  xyz@localhost on your_db
73 Query    SET NAMES utf8mb4
74 Connect  xyz@localhost on your_db
75 Connect  xyz@localhost on your_db
74 Quit 
75 Prepare  SELECT email FROM customer WHERE email=? LIMIT ?
75 Execute  SELECT email FROM customer WHERE email='a@b.co' LIMIT 5
75 Close stmt   
75 Quit 
73 Quit 

Any new queries your app makes will automatically pop into view, as long as you continue tailing the log. To exit the tail, hit cmd/ctrl c.

Notes

  1. Careful: this log file can get huge. I’m only running this on my dev server.
  2. Log file getting too big? Truncate it. That means the file stays, but the contents are deleted. truncate --size 0 mysql.log.
  3. Cool that the log file lists the mysql connections. I know one of those is from my legacy mysqli code from which I'm transitioning. The third is from my new PDO connection. However, not sure where the second is coming from. If you know a quick way to find it, let me know.

Credit & thanks

Huge shout out to Nathan Long’s answer above for the inspo to figure this out on Ubuntu. Also to dikirill for his comment on Nathan’s post which lead me to this solution.

Love you stackoverflow!

elbowlobstercowstand
  • 3,812
  • 1
  • 25
  • 22
0

The problem I had with the solution to catch PDO exemptions for debuging purposes is that it only caught PDO exemptions (duh), but didn't catch syntax errors which were registered as php errors (I'm not sure why this is, but "why" is irrelevant to the solution). All my PDO calls come from a single table model class that I extended for all my interactions with all tables... this complicated things when I was trying to debug code, because the error would register the line of php code where my execute call was called, but didn't tell me where the call was, actually, being made from. I used the following code to solve this problem:

/**
 * Executes a line of sql with PDO.
 * 
 * @param string $sql
 * @param array $params
 */
class TableModel{
    var $_db; //PDO connection
    var $_query; //PDO query

    function execute($sql, $params) { 
        //we're saving this as a global, so it's available to the error handler
        global $_tm;
        //setting these so they're available to the error handler as well
        $this->_sql = $sql;
        $this->_paramArray = $params;            

        $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->_query = $this->_db->prepare($sql);

        try {
            //set a custom error handler for pdo to catch any php errors
            set_error_handler('pdoErrorHandler');

            //save the table model object to make it available to the pdoErrorHandler
            $_tm = $this;
            $this->_query->execute($params);

            //now we restore the normal error handler
            restore_error_handler();
        } catch (Exception $ex) {
            pdoErrorHandler();
            return false;
        }            
    }
}

So, the above code catches BOTH PDO exceptions AND php syntax errors and treats them the same way. My error handler looks something like this:

function pdoErrorHandler() {
    //get all the stuff that we set in the table model
    global $_tm;
    $sql = $_tm->_sql;
    $params = $_tm->_params;
    $query = $tm->_query;

    $message = 'PDO error: ' . $sql . ' (' . implode(', ', $params) . ") \n";

    //get trace info, so we can know where the sql call originated from
    ob_start();
    debug_backtrace(); //I have a custom method here that parses debug backtrace, but this will work as well
    $trace = ob_get_clean();

    //log the error in a civilized manner
    error_log($message);

    if(admin(){
        //print error to screen based on your environment, logged in credentials, etc.
        print_r($message);
    }
}

If anyone has any better ideas on how to get relevant info to my error handler than setting the table model as a global variable, I would be happy to hear it and edit my code.

Troy Knapp
  • 517
  • 3
  • 8
  • 17
0

this code works great for me :

echo str_replace(array_keys($data), array_values($data), $query->queryString);

Don't forget to replace $data and $query by your names

user3553866
  • 316
  • 1
  • 3
  • 15
0

i use this class to debug PDO (with Log4PHP)

<?php

/**
 * Extends PDO and logs all queries that are executed and how long
 * they take, including queries issued via prepared statements
 */
class LoggedPDO extends PDO
{

    public static $log = array();

    public function __construct($dsn, $username = null, $password = null, $options = null)
    {
        parent::__construct($dsn, $username, $password, $options);
    }

    public function query($query)
    {
        $result = parent::query($query);
        return $result;
    }

    /**
     * @return LoggedPDOStatement
     */
    public function prepare($statement, $options = NULL)
    {
        if (!$options) {
            $options = array();
        }
        return new \LoggedPDOStatement(parent::prepare($statement, $options));
    }
}

/**
 * PDOStatement decorator that logs when a PDOStatement is
 * executed, and the time it took to run
 * @see LoggedPDO
 */
class LoggedPDOStatement
{

    /**
     * The PDOStatement we decorate
     */
    private $statement;
    protected $_debugValues = null;

    public function __construct(PDOStatement $statement)
    {
        $this->statement = $statement;
    }

    public function getLogger()
    {
        return \Logger::getLogger('PDO sql');
    }

    /**
     * When execute is called record the time it takes and
     * then log the query
     * @return PDO result set
     */
    public function execute(array $params = array())
    {
        $start = microtime(true);
        if (empty($params)) {
            $result = $this->statement->execute();
        } else {
            foreach ($params as $key => $value) {
                $this->_debugValues[$key] = $value;
            }
            $result = $this->statement->execute($params);
        }

        $this->getLogger()->debug($this->_debugQuery());

        $time = microtime(true) - $start;
        $ar = (int) $this->statement->rowCount();
        $this->getLogger()->debug('Affected rows: ' . $ar . ' Query took: ' . round($time * 1000, 3) . ' ms');
        return $result;
    }

    public function bindValue($parameter, $value, $data_type = false)
    {
        $this->_debugValues[$parameter] = $value;
        return $this->statement->bindValue($parameter, $value, $data_type);
    }

    public function _debugQuery($replaced = true)
    {
        $q = $this->statement->queryString;

        if (!$replaced) {
            return $q;
        }

        return preg_replace_callback('/:([0-9a-z_]+)/i', array($this, '_debugReplace'), $q);
    }

    protected function _debugReplace($m)
    {
        $v = $this->_debugValues[$m[0]];

        if ($v === null) {
            return "NULL";
        }
        if (!is_numeric($v)) {
            $v = str_replace("'", "''", $v);
        }

        return "'" . $v . "'";
    }

    /**
     * Other than execute pass all other calls to the PDOStatement object
     * @param string $function_name
     * @param array $parameters arguments
     */
    public function __call($function_name, $parameters)
    {
        return call_user_func_array(array($this->statement, $function_name), $parameters);
    }
}
Janos Szabo
  • 1,304
  • 1
  • 15
  • 14
0

In Debian NGINX environment i did the following.

Goto /etc/mysql/mysql.conf.d edit mysqld.cnf if you find log-error = /var/log/mysql/error.log add the following 2 lines bellow it.

general_log_file        = /var/log/mysql/mysql.log
general_log             = 1

To see the logs goto /var/log/mysql and tail -f mysql.log

Remember to comment these lines out once you are done with debugging if you are in production environment delete mysql.log as this log file will grow quickly and can be huge.

khan
  • 1,099
  • 1
  • 11
  • 21