0

I'm trying to execute a raw query with a prepared statement in PDO-like style using unquoted params (cases 1, 1A), anyway it throws an exception:

An exception occurred while executing 'SELECT * FROM pages WHERE title LIKE :title': You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ':title' at line 1

Also, quoting named param doesn't work (case 2), it doesn't throw an exception but doesn't find anything as well.

Using unnamed/numbered and unquoted params (cases 3, 3A) or executeQuery() instead of prepare() (case 4) works as required. Especially that I would like to use named params, the last one is my choice.

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

... 

public function queryPagesByTitle(string $title = null): array
{
    /** @var Connection $conn */
    $conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');

//  Case 1: DOESN'T work with non-quoted params
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE :title");
    $stmt->execute(['title' => $title]);

//  Case 1A: DOESN'T work with non-quoted params
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE :title");
    $stmt->bindValue('title', $title, \PDO::PARAM_STR);
    $stmt->execute();

//  Case 1B: DOESN'T work with non-quoted,unique params
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE :dcUniqueParam");
    $stmt->bindParam('dcUniqueParam', $title, \PDO::PARAM_STR);
    $stmt->execute();

//  Case 1C: DOESN'T work with non-quoted,unique params even with :colon while binding
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE :dcUniqueParam");
    $stmt->bindParam(':dcUniqueParam', $title, \PDO::PARAM_STR);

//  Case 2: DOESN'T work with quoted params neither, doesn't throw an exception, but doesn;t find anything
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE ':title'");
    $stmt->execute(['title' => $title]);

//  Case 3: Works with numbered param(s)
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE ?");
    $stmt->execute([1 => $title]);

//  Case 3A: Works with numbered param(s)
    $stmt = $conn->prepare("SELECT * FROM pages WHERE title LIKE ?");
    $stmt->bindParam(1, $title, \PDO::PARAM_STR);
    $stmt->execute();

//  Case 4: Works with non-quoted named param(s)
    $stmt = $conn->executeQuery(
        "SELECT uid, title FROM pages WHERE title LIKE :title",
        ['title' => $title],
        [\PDO::PARAM_STR]
    );
    return $stmt->fetchAll(FetchMode::ASSOCIATIVE);
}

Several questions

  1. Why the first case doesn't work as I would expect it after PDO inheritance or how also Doctrine actualy does it?
  2. Are there some disadvantages (if any) of using executeQuery() instead of prepare()?
  3. Should I use prepare() with numbered params instead?
  4. Is there any significant difference between using raw queries instead of QueryBuilder?

Note

I'm aware that for proper work with models data and repositories I can/should use common QueryBuilder interface. This case is for some raw data in my DB that doesn't use data mapping and I'm looking for some performance improvement. pages table is used here only for demonstrating the concept.

Oliver Hader
  • 4,093
  • 1
  • 25
  • 47
biesior
  • 55,576
  • 10
  • 125
  • 182
  • my bet is that it has to do with defaults being applied in the cases where no explicit type is given, along with the quirkiness of sql `LIKE` – Nikos M. Jul 28 '20 at 13:08
  • 1
    @NikosM. `$stmt->bindValue('title', $title, \PDO::PARAM_STR);` also throws an exception, added case 1A – biesior Jul 28 '20 at 13:19
  • I guess digging into the framework code is needed to make sense of that. – Nikos M. Jul 28 '20 at 13:25
  • I woud guess there is a conflict with the parameter names as a field. probably all 'title' is replaced with `'tablename.title'` – Bernd Wilke πφ Jul 28 '20 at 15:10
  • @BerndWilkeπφ logical conclusion, however still not this. I added case 1B with some quite unique param, still the same exception. as for other 1x cases. – biesior Jul 28 '20 at 15:23

1 Answers1

2

In the end it all comes down to a PDO-like statement, however with an essential difference between using mysqli as driver (https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php) or pdo_mysql as driver (https://www.php.net/manual/en/pdo.prepared-statements.php).

The important aspect is mentioned in PDO docs (https://www.php.net/manual/en/pdo.prepare.php):

PDO will emulate prepared statements/bound parameters for drivers that do not natively support them, and can also rewrite named or question mark style parameter markers to something more appropriate, if the driver supports one style but not the other.

The code snippets given in the original question are working when using pdo_mysql as driver - instead of mysqli which can be configured in typo3conf/LocalConfiguration.php

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] = 'pdo_mysql';

Now focussing on the internal details of Doctrine DBAL, which is also just a wrapper for mysqli or pdo_mysql - internally DBAL is using positioned parameters ? and converts named parameters accordingly.

Actually that happens in Doctrine DBAL's Connection - named parameters are transformed to positioned parameters (no matter which database driver has been used):

SELECT * FROM `pages` WHERE `title` LIKE :dcValue1

gets converted to

SELECT * FROM `pages` WHERE `title` LIKE ?

Summary


Besides that, since you're in a TYPO3 environment already, you might want to use it's QueryBuilder that uses prepared statements internally as well.

public function queryPagesByTitle(string $title = null): array
{
    $builder = GeneralUtility::makeInstance(ConnectionPool::class)
        ->getQueryBuilderForTable('page');
    $stmt = $builder->select('*')
        ->from('pages')
        ->where($builder->expr()->like(
            'title',
            $builder->createNamedParameter(
                '%' . $builder->escapeLikeWildcards($title) . '%',
                \PDO::PARAM_STR
            )
        ))
        ->execute();
    return $stmt->fetchAll(FetchMode::ASSOCIATIVE) ?? [];
}
Oliver Hader
  • 4,093
  • 1
  • 25
  • 47
  • Thanks for your effort, but you are wrong. Although in PDO it's not documented, as stated in many places binding param doesn't require the colon in common PDO that works without a problem `$stmt->bindValue('deleted', 0, PDO::PARAM_INT);` (tested many times). Sorry, I tested `:colon` case before, but didn't add in my code, now updated check case `1C` in my question. – biesior Jul 28 '20 at 17:48
  • As I mentioned in my question I know the QueryBuilder and with no doubt that's what I'm using when working in general TYPO3 context. However for some queries in my database, especially in tables that are not TYPO3 specific (no pid, hidden, deleted, sysl_language etc, etc) using it can be just big overload. – biesior Jul 28 '20 at 17:52
  • Thx for explanation, I actually never used it without `:colon` - now I learned it works as well ;-) I guess you're using `mysqli` as database driver which is slightly different to `pdo_mysql`. – Oliver Hader Jul 28 '20 at 19:34
  • No, no, I'm still talking about `PDO`, actually, `mysqli` doesn't support named params natively at all. That's just funny fact which is undocumented in PDO ;) – biesior Jul 28 '20 at 19:39
  • The problem with TYPO3 usage of Doctrine is it's still doesn't matter if we are trying to use it with or without a colon, it always fails. So correct question is according to old, known TYPO3 slogan, "Is this a bug or a feature?" – biesior Jul 28 '20 at 19:44
  • I've updated the answer concerning PHP's native implementation. Internally DBAL uses `?` (tested with `mysqli` and `pdo_mysql`). Your initial examples however worked without any problem using `pdo_mysql` - but did not with `mysqli` - using Doctrine DBAL `v2.10.0`. – Oliver Hader Jul 28 '20 at 20:11
  • 1
    The clue is switching from `mysqli` to `pdo_mysql` in `typo3conf/LocalConfiguration.php` In such situations, all cases works except of these with quoted named param (which is valid). Thank you very much for your effort and investigation. Can you update your answer for others with info about setting `$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] = 'pdo_mysql';`? – biesior Jul 28 '20 at 20:55