4

I am working on a simple SQL debugger which will accept parameterized variables and try to replace them accordingly so that if a piece of SQL has an issue then I can copy+paste it directly into my RDBMS to work with the query and hopefully debug an issue quicker.

So far I essentially have this, but it is replacing too much:

<?php
$sql = "select *
  from table_name
 where comment like :a and
       email = :b and
       status = :c";

$patterns = array();
$patterns[0] = '/:a/';
$patterns[1] = '/:b/';
$patterns[2] = '/:c/';

$replacements = array();
$replacements[0] = "'%that is a nice :b but this one%'";
$replacements[1] = "'monkeyzeus@example.com'";
$replacements[2] = "'active'";

echo preg_replace($patterns, $replacements, $sql);

Resulting in

select *
  from table_name
 where comment like '%that is a nice 'monkeyzeus@example.com' but this one%' and
       email = 'monkeyzeus@example.com' and
       status = 'active'

Notice that 'monkeyzeus@example.com' from position 1 is making it into the :b from position 0.

I've found this question, Can preg_replace make multiple search and replace operations in one shot?, but I cannot make heads or tails of it because I am certainly no regex expert.


Update. Just wanted to share the final product:

function debug_sql($sql = NULL, $params = NULL)
{
    return (
        $sql !== NULL && is_array($params) && $params ? // $sql and $params is required
        strtr( // Feed this function the sql and the params which need to be replaced
            $sql,
            array_map( // Replace single-quotes within the param items with two single-quotes and surround param in single-quotes
                function($p)
                {
                    return "'".str_replace("'", "''", $p)."'"; // Basic Oracle escaping
                },
                $params
            )
        ) :
        $sql
    );
}
Community
  • 1
  • 1
MonkeyZeus
  • 20,375
  • 4
  • 36
  • 77
  • 2
    The question you found also uses multiple replacement rounds, one after the other so that does not help you. A possible solution would be to do it in 2 rounds: First replace the placeholders with some kind of hash / placeholder that will never appear and then replace these with your replacement strings. Note that you'd need also word-breaks around your pattern strings as placeholders could be `:b`, `:bb`, etc. – jeroen Mar 30 '16 at 13:53
  • 1
    @jeroen That is a fantastic sounding solution to my issue; double-replace with a hash round. Fortunately I am one step ahead with the `:b` and `:bb` issue which you've pointed out. I am sorting the parameters by the `strlen()` of their key so that they will always be replaced in this order: `$patterns[0] = '/:bb/'; $patterns[1] = '/:b/';` – MonkeyZeus Mar 30 '16 at 14:05
  • @jeroen Thank you for the great suggestion. I have posted an answer below. If you have a minute I would certainly love to get your feedback about any issues which you might spot which I do not. Specifically whether my "hash" is going to be unique enough for the foreseeable future. – MonkeyZeus Mar 30 '16 at 14:36

3 Answers3

2

After some insightful suggestions from jeroen:

First replace the placeholders with some kind of hash / placeholder that will never appear and then replace these with your replacement strings.

I have come up with this and it seems to work for all of my test cases:

<?php
$sql = "select *
  from table_name
 where comment like :a and
       email = :b and
       status = :c and
       something = :bb";

$patterns = array();
$replacements = array();

$patterns[0][0] = '/(:a)\\b/';
$patterns[0][1] = '/(:b)\\b/'; // Use word-boundary to prevent :b from being found in :bb
$patterns[0][2] = '/(:c)\\b/';
$patterns[0][3] = '/(:bb)\\b/';

$replacements[0][0] = str_replace('.', '', uniqid('', TRUE));
$replacements[0][1] = str_replace('.', '', uniqid('', TRUE));
$replacements[0][2] = str_replace('.', '', uniqid('', TRUE));
$replacements[0][3] = str_replace('.', '', uniqid('', TRUE));

$patterns[1][0] = '/('.$replacements[0][0].')\\b/';
$patterns[1][1] = '/('.$replacements[0][1].')\\b/';
$patterns[1][2] = '/('.$replacements[0][2].')\\b/';
$patterns[1][3] = '/('.$replacements[0][3].')\\b/';

$replacements[1][0] = "'%that is a nice :b but this one%'";
$replacements[1][1] = "'monkeyzeus@example.com'";
$replacements[1][2] = "'active'";
$replacements[1][3] = "'another thing'";

$sql = preg_replace($patterns[0], $replacements[0], $sql);
$sql = preg_replace($patterns[1], $replacements[1], $sql);

echo $sql;

The only way that this could fail is if the user is querying to the exact output of str_replace('.', '', uniqid('', TRUE)) at the time of processing.

Community
  • 1
  • 1
MonkeyZeus
  • 20,375
  • 4
  • 36
  • 77
  • @Martin works for me in my simple debugger and when I test it at http://sandbox.onlinephpfunctions.com/ :) – MonkeyZeus Mar 30 '16 at 14:51
  • It's just a case of clarity; as `I have come up with this:` doesn't seem to me to be a definitive answer, but more or a suggestion. If your answer works can you edit your answer with a note that it does indeed work? Cheers :-) – Martin Mar 30 '16 at 14:53
  • I would not prefix the generated id's with a colon, if I am reading this correctly, it will go horribly wrong if your first replacement generates an id that starts with `:b...` or `:c...` in this example :-) – jeroen Mar 30 '16 at 14:56
  • @jeroen Great catch! Fixed. – MonkeyZeus Mar 30 '16 at 14:58
  • Thanks, you've made a crusty fart very happy. +1. – Martin Mar 30 '16 at 14:59
  • @jeroen I took your advice and looked at word-boundaries for avoiding the `:b` and `:bb` issue. It's going to save me a `foreach()` loop and multiple calls to `strlen()` within my debugger. Thanks again! You've been awesome! – MonkeyZeus Mar 30 '16 at 18:06
2

There is a special function exactly for this case: strtr — Translate characters or replace substrings http://php.net/manual/en/function.strtr.php

<?php

$sql = "select * from table_name where comment like :a and email = :b and status = :c";

$map = [
    ':a' => "'%that is a nice :b but this one%'",
    ':b' => "'monkeyzeus@example.com'",
    ':c' => "'active'"
];

echo strtr($sql, $map);
Axalix
  • 2,831
  • 1
  • 20
  • 37
  • Would this be able to handle `$sql = "select * from table_name where comment like :a and email = :b and status = :c and something = :bb";` along with `':bb' => "'something else'"` being added to the end of the array? – MonkeyZeus Mar 30 '16 at 19:03
  • @MonkeyZeus yes, that will also work fine. `echo strtr(':a :aa :a', [':a' => ":apple", ':aa' => "pear"]);` will print *:apple pear :apple* – Axalix Mar 30 '16 at 19:10
  • 2
    PHP has a function for **EVERYTHING!** lol. It is highly likely that I will be accepting your answer. It is quite clean and runs much faster than the solution I posted. Kudos :) – MonkeyZeus Mar 30 '16 at 19:15
  • True. This is what the documentation says about `str_replace` (a big friend of `strtr`) *If you don't need fancy replacing rules (like regular expressions), you should always use this function instead of preg_replace().* – Axalix Mar 30 '16 at 19:18
  • This is absolutely fantastic, I've withered down my `debug_sql()` function to one line of ternary. `function debug_sql($sql = NULL, $params = NULL){return ($sql !== NULL && is_array($params) && $params ? strtr($sql, array_map(function($p){return "'".str_replace("'", "''", $p)."'";}, $params)) : $sql);}` – MonkeyZeus Mar 30 '16 at 19:28
1

An alternative approach without regexp is to recursively explode/implode the query:

$sql = "select * from table_name where comment like :a and email = :b and status = :c ";

$patterns = array();
$patterns[0] = ' :a ';
$patterns[1] = ' :b ';
$patterns[2] = ' :c ';

$replacements = array();
$replacements[0] = " '%that is a nice :b but this one%' ";
$replacements[1] = " 'monkeyzeus@example.com' ";
$replacements[2] = " 'active' ";

function replace($substr, $replacement, $subj) {
    if (empty($substr)) {
        return $subj;
    }
    $s = array_shift($substr);
    $r = array_shift($replacement);
    foreach($subj as &$str) {
        $str = implode($r, replace($substr, $replacement, explode($s, $str)));
    }
    return $subj;
}

echo replace($patterns, $replacements, [$sql])[0];
Alex Blex
  • 34,704
  • 7
  • 48
  • 75
  • Thank you for the answer. I can see that you've put quite a bit of effort into your code and it does appear to work for the limited test cases which I ran it against but unfortunately it does add much more lines of code than I want to use in my debugger. +1 :-) – MonkeyZeus Mar 30 '16 at 17:49