PDO::prepare()に不正なSQLを渡したときの挙動

このページは、PHPの PDO::prepare() に不正なSQLを渡したときの挙動について検証してまとめたページです。

注意

  • 下記のバージョンで検証したものです。別のバージョンや組み合わせでは違う動作になるかもしれません。
    • PHP 8.1 + MySQL 8.0.31
    • PHP 8.1 + MariaDB 10.10.2
    • PHP 8.1 + PostgreSQL 15.1
    • PHP 8.1 + SQLite 3.34.1

検証結果

環境 挙動 (結果)
MySQL (PDO::ATTR_EMULATE_PREPARES => true) エラーにならない
〃 (PDO::ATTR_EMULATE_PREPARES => false) 構文エラー
MariaDB (PDO::ATTR_EMULATE_PREPARES => true) エラーにならない
〃 (PDO::ATTR_EMULATE_PREPARES => false) 構文エラー
PostgreSQL エラーにならない
SQLite 構文エラー
  • エラーになっていない環境では、prepare()の時点ではまだ各DB側にSQLが渡っていません。
  • prepare() でエラーにならない場合でも、execute() ではエラーになります。(各DB側にSQLが渡るため)
  • PostgreSQL, SQLiteでは PDO::ATTR_EMULATE_PREPARES は利用できません。

MySQL, MariaDB (PDO::ATTR_EMULATE_PREPARES => true)

検証コード
<?php
$dsn = 'mysql:host=localhost;dbname=demo'; // DSN
$user = 'root'; // ユーザー
$pass = 'pass'; // パスワード
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => true // デフォルトでtrueのため指定しなくてもいい
]);

$stmt = $pdo->prepare('malformed sql'); // 成功

MySQL, MariaDB (PDO::ATTR_EMULATE_PREPARES => false)

検証コード
<?php
$dsn = 'mysql:host=localhost;dbname=demo'; // DSN
$user = 'root'; // ユーザー
$pass = 'pass'; // パスワード
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false
]);

$stmt = $pdo->prepare('malformed sql'); // エラー
エラーメッセージ (MySQL)
Fatal error: Uncaught PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 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 'malformed sql' at line 1 in /var/www/html/index.php:10 Stack trace: #0 /var/www/html/index.php(10): PDO->prepare('malformed sql') #1 {main} thrown in /var/www/html/index.php on line 10
エラーメッセージ (MariaDB)
Fatal error: Uncaught PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'malformed sql' at line 1 in /var/www/html/index.php:10 Stack trace: #0 /var/www/html/index.php(10): PDO->prepare('malformed sql') #1 {main} thrown in /var/www/html/index.php on line 10

ソース

ext/pdo_mysql/mysql_driver.c (PHP 8.1.14)
// prepare() → PHP_METHOD(PDO, prepare)[ext/pdo/pdo_dbh.c] → dbh->methods->preparer() → この関数
static bool mysql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)

// 略

	// エミュレートモードの場合は処理がスキップされる
	if (H->emulate_prepare) {
		goto end;
	}

// 略

	// 非エミュレートモードの場合はここでステートメントを準備する (不正なSQLだとここでエラーになる)
	if (mysql_stmt_prepare(S->stmt, ZSTR_VAL(sql), ZSTR_LEN(sql))) {

PostgreSQL

検証コード
<?php
$dsn = 'pgsql:host=localhost;dbname=demo'; // DSN
$user = 'postgres'; // ユーザー
$pass = 'postgres'; // パスワード
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

$stmt = $pdo->prepare('malformed sql'); // 成功

ソース

ext/pdo_pgsql/pgsql_driver.c (PHP 8.1.14)
// prepare() → PHP_METHOD(PDO, prepare)[ext/pdo/pdo_dbh.c] → dbh->methods->preparer() → この関数
// PDO::prepare()の段階ではPQprepare()等は行われない
static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
{
	pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
	pdo_pgsql_stmt *S = ecalloc(1, sizeof(pdo_pgsql_stmt));
	int scrollable;
	int ret;
	zend_string *nsql = NULL;
	int emulate = 0;
	int execute_only = 0;

	S->H = H;
	stmt->driver_data = S;
	stmt->methods = &pgsql_stmt_methods;

	scrollable = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR,
		PDO_CURSOR_FWDONLY) == PDO_CURSOR_SCROLL;

	if (scrollable) {
		if (S->cursor_name) {
			efree(S->cursor_name);
		}
		spprintf(&S->cursor_name, 0, "pdo_crsr_%08x", ++H->stmt_counter);
		emulate = 1;
	} else if (driver_options) {
		if (pdo_attr_lval(driver_options, PDO_ATTR_EMULATE_PREPARES, H->emulate_prepares) == 1) {
			emulate = 1;
		}
		if (pdo_attr_lval(driver_options, PDO_PGSQL_ATTR_DISABLE_PREPARES, H->disable_prepares) == 1) {
			execute_only = 1;
		}
	} else {
		emulate = H->disable_native_prepares || H->emulate_prepares;
		execute_only = H->disable_prepares;
	}

	if (!emulate && PQprotocolVersion(H->server) <= 2) {
		emulate = 1;
	}

	if (emulate) {
		stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
	} else {
		stmt->supports_placeholders = PDO_PLACEHOLDER_NAMED;
		stmt->named_rewrite_template = "$%d";
	}

	ret = pdo_parse_params(stmt, sql, &nsql);

	if (ret == -1) {
		/* couldn't grok it */
		strcpy(dbh->error_code, stmt->error_code);
		return false;
	} else if (ret == 1) {
		/* query was re-written */
		S->query = nsql;
	} else {
		S->query = zend_string_copy(sql);
	}

	if (!emulate && !execute_only) {
		/* prepared query: set the query name and defer the
		   actual prepare until the first execute call */
		spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
	}

	return true;
}

SQLite

検証コード
<?php
$dsn = 'sqlite:memory'; // DSN
$pdo = new PDO($dsn, '', '', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

$stmt = $pdo->prepare('malformed sql'); // エラー
エラーメッセージ (SQLite)
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 near "malformed": syntax error in /var/www/html/index.php:8 Stack trace: #0 /var/www/html/index.php(8): PDO->prepare('malformed sql') #1 {main} thrown in /var/www/html/index.php on line 8

ソース

ext/pdo_sqlite/sqlite_driver.c (PHP 8.1.14)
// prepare() → PHP_METHOD(PDO, prepare)[ext/pdo/pdo_dbh.c] → dbh->methods->preparer() → この関数
static bool sqlite_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
{
// 略

	// ステートメントを準備する (不正なSQLだとここでエラーになる)
	i = sqlite3_prepare_v2(H->db, ZSTR_VAL(sql), ZSTR_LEN(sql), &S->stmt, &tail);
	if (i == SQLITE_OK) {
		return true;
	}