[PHP][SQLite] SQLiteのデータベースには複数の形式があり、ドライバが対応できていなければ扱えない。
問題点
WebアプリケーションでPHPからSQLiteにPDOで接続しようと思い、
$pdo = new PDO('sqlite:/home/tom/projects/MagnesiumRider/mgr/db/data'); $stmt = $pdo->prepare("INSERT INTO ACCOUNT (NAME, PASSWORD) VALUES (:NAME, :PASSWORD)"); $stmt->bindValue(':NAME', $name); $stmt->bindValue(':PASSWORD', $password); $stmt->execute();
というコードを書いて実行したところ、
Fatal error: Call to a member function bindValue() on a non-object in /home/tom/projects/MagnesiumRider/mgr/app/action/Account/Add/Do.php on line 107
というエラーになりました。
原因
sqlite3で作成したデータベースを、PDOのSQLiteドライバが読むことができないためです。
対処方法
PHPを使ってPDO経由でデータベースを作成するという方法があります。例えば、以下のようなスクリプトを作成して、
<?php $pdo = new PDO('sqlite:db/data'); $stmt = $pdo->prepare('create table account (name, password)'); $stmt->execute(); ?>
次のように実行します。
$ php setupdb.php
このようにして作成されたデータベースは、以前のバージョンの形式で作成されます。なお、コマンドラインからPHPを実行した場合、Ubuntuでは設定ファイルは/etc/php5/apache2/php.iniではなく、/etc/php5/cli/php.iniになります。そちらに、
extension=pdo.so extension=pdo_sqlite.so
を追加するのを忘れないでください。
原因の特定方法
メッセージには「オブジェクトではないものに対してbindValueを呼び出した」とあります。107行めというのは、
$stmt->bindValue(':NAME', $name);
です。どうやらこの$stmt変数が期待した値になっていないようです。そこで、var_dump関数を使って、変数の内容を表示させてみます。念のため、変数$pdoについても表示させてみます。コードは以下の通りです。
$pdo = new PDO('sqlite:/home/tom/projects/MagnesiumRider/mgr/db/data'); var_dump($pdo); $stmt = $pdo->prepare("INSERT INTO ACCOUNT (NAME, PASSWORD) VALUES (:NAME, :PASSWORD)"); var_dump($stmt);
結果は、
object(PDO)#16 (0) { } bool(false)
となりました。どうやら、"object(PDO)#16 (0) {}"が$pdoで、"bool(false)"が$stmtのようです。つまり、$stmt変数は、SQL文を実行させるためのなんらかのオブジェクトであることを期待していたのですが、実際にはfalseになっているということです。$pdo->prepareメソッドが、期待どおりに動作していないようです。これを確かめることにします。
PDOとPDO_SQLITEは"pecl install"でインストールしましたが、このpeclのヘルプを見ると、installの代わりにdownloadを指定すれば、ソースコードを取り寄せることができそうです。実際にやってみると、PDO-1.0.3.tgzとPDO_SQLITE-1.0.1.tgzを得られました。
$ pecl download pdo pdo_sqlite [~/tmp] downloading PDO-1.0.3.tgz ... Starting to download PDO-1.0.3.tgz (52,613 bytes) .............done: 52,613 bytes downloading PDO_SQLITE-1.0.1.tgz ... Starting to download PDO_SQLITE-1.0.1.tgz (868,469 bytes) ...done: 868,469 bytes File /home/tom/tmp/PDO-1.0.3.tgz downloaded File /home/tom/tmp/PDO_SQLITE-1.0.1.tgz downloaded
それでは、問題になっている$pdo->prepareというのは、ソースコードではどこにあたるのでしょうか? PDOのソースコード全体を"prepare"で検索して調べてみます。
$ cgrep -wr prepare [~/projects/PDO-1.0.3] ./pdo_stmt.c:301: /* if you prepare and then execute passing an array of params keyed by names, ./php_pdo_driver.h:236:/* prepare a statement and stash driver specific portion into stmt */ ./php_pdo_driver.h:553: * emulate prepare and bind on its behalf */ ./php_pdo_driver.h:585: /* the copy of the query with expanded binds ONLY for emulated-prepare drivers */ ./pdo_dbh.c:475:/* {{{ proto object PDO::prepare(string statment [, array options]) ./pdo_dbh.c:477:static PHP_METHOD(PDO, prepare) ./pdo_dbh.c:1078: PHP_ME(PDO, prepare, NULL, ZEND_ACC_PUBLIC)
ちなみに、ここで使用しているcgrepというコマンドは自作のスクリプトで、C言語のソースコードに限定してgrepできるというものです。http://nekomimists.ddo.jp/~tom/toolsからダウンロードできます。興味のある方はご参照ください。
さて、検索結果にある
./pdo_dbh.c:477:static PHP_METHOD(PDO, prepare)
というのが、メソッドでPDOでprepareなので、それらしいです。
まず、本当にこの関数が呼び出されているのかを確認します。こういうデバッグの場合、printfを入れて出力を見る「printfデバッグ」がよくやられる手のひとつですが、いま対象にしているプログラムはWebアプリケーションなので、この方法は使えません(実際にprintfを入れてみましたが、何も変化しませんでした)。代わりの方法として考えられるのは、
- php_printf関数を使用する。
- ファイルにログを出力する。
- Webアプリケーションとしてではなく、コマンドラインから実行してprintfを使用する。
などがあると思います。ここでは、php_printf関数を使用することにします。php_printf関数は、Webアプリケーションのレスポンスに書き出すことができるprintfです。なんでこんな関数があるのが分かったかというと、先ほど出てきたPHPのvar_dump関数がヒントでした。var_dump関数を検索すると、
$ cgrep -w var_dump [~/projects/php5-5.1.6] (略) ./ext/standard/var.c:177:PHP_FUNCTION(var_dump) (略)
で、ext/standard/var.cに実体があるのが分かります。これを見ると、以下のようになっています。
/* {{{ proto void var_dump(mixed var) Dumps a string representation of variable to output */ PHP_FUNCTION(var_dump) { zval ***args; int argc; int i; argc = ZEND_NUM_ARGS(); args = (zval ***)safe_emalloc(argc, sizeof(zval **), 0); if (ZEND_NUM_ARGS() == 0 || zend_get_parameters_array_ex(argc, args) == FAILURE) { efree(args); WRONG_PARAM_COUNT; } for (i=0; i<argc; i++) php_var_dump(args[i], 1 TSRMLS_CC); efree(args); }
var_dumpの中から呼び出しているphp_var_dump関数を見てみると、
PHPAPI void php_var_dump(zval **struc, int level TSRMLS_DC) { HashTable *myht = NULL; char *class_name; zend_uint class_name_len; int (*php_element_dump_func)(zval**, int, va_list, zend_hash_key*); if (level > 1) { php_printf("%*c", level - 1, ' '); } switch (Z_TYPE_PP(struc)) { case IS_BOOL: php_printf("%sbool(%s)\n", COMMON, Z_LVAL_PP(struc)?"true":"false"); break; case IS_NULL: php_printf("%sNULL\n", COMMON); break; case IS_LONG: php_printf("%sint(%ld)\n", COMMON, Z_LVAL_PP(struc)); break; (略)
と、このように、php_printf関数を使って出力しています。php_printf関数は、探してみるとmain/main.cにあって(探す手順は省略します)、
/* {{{ php_printf */ PHPAPI int php_printf(const char *format, ...) { va_list args; int ret; char *buffer; int size; TSRMLS_FETCH(); va_start(args, format); size = vspprintf(&buffer, 0, format, args); ret = PHPWRITE(buffer, size); efree(buffer); va_end(args); return ret; } /* }}} */
となっています。
そこで試しに、適当なphp_printf関数を入れてみます。
/* {{{ proto object PDO::prepare(string statment [, array options]) Prepares a statement for execution and returns a statement object */ static PHP_METHOD(PDO, prepare) { php_printf("Enter PHP_METHOD(PDO, prepare)<br>\n"); pdo_dbh_t *dbh = zend_object_store_get_object(getThis() TSRMLS_CC); pdo_stmt_t *stmt;
PDOは、"pecl install"コマンドでコンパイルされます。
$ tar zcf PDO-1.0.3.tgz PDO-1.0.3 [~/projects] $ sudo pecl uninstall pdo pdo_sqlite [~/projects] Password: uninstall ok: channel://pear.php.net/PDO_SQLITE-1.0.1 uninstall ok: channel://pear.php.net/PDO-1.0.3 $ sudo pecl install PDO-1.0.3.tgz [~/projects] 12 source files, building (略) install ok: channel://pear.php.net/PDO-1.0.3
これで件のWebアプリケーションを実行してみます。次のようなメッセージが、Webブラウザに表示されました。
object(PDO)#16 (0) { } Enter PHP_METHOD(PDO, prepare)
bool(false)
Fatal error: Call to a member function bindValue() on a non-object in /home/tom/projects/MagnesiumRider/mgr/app/action/Account/Add/Do.php on line 109
PHP_METHOD(PDO, prepare)が実行されるのは、間違いないようです。では、関数が終了する箇所に適当なデバッグ文を入れ、どこでfalseを返しているのか調べます。関数は、以下の様になりました。
/* {{{ proto object PDO::prepare(string statment [, array options]) Prepares a statement for execution and returns a statement object */ static PHP_METHOD(PDO, prepare) { php_printf("Enter PHP_METHOD(PDO, prepare)<br>\n"); pdo_dbh_t *dbh = zend_object_store_get_object(getThis() TSRMLS_CC); pdo_stmt_t *stmt; char *statement; int statement_len; zval *options = NULL, **opt, **item, *ctor_args; zend_class_entry *dbstmt_ce, **pce; if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|a", &statement, &statement_len, &options)) { php_printf("RETURN_FALSE: 1<br>\n"); RETURN_FALSE; } (略) if (dbh->methods->preparer(dbh, statement, statement_len, stmt, options TSRMLS_CC)) { pdo_stmt_construct(stmt, return_value, dbstmt_ce, ctor_args TSRMLS_CC); php_printf("RETURN_FALSE: 7<br>\n"); return; } PDO_HANDLE_DBH_ERR(); /* kill the object handle for the stmt here */ zval_dtor(return_value); php_printf("RETURN_FALSE: 8<br>\n"); RETURN_FALSE; } /* }}} */
なお、この関数の戻り値はvoidなのに、RETURN_FALSEという値を返しているかのようなマクロが使われていますが、これはRETURN_FALSEがPHPのZend/zend_API.hで、
#define RETURN_FALSE { RETVAL_FALSE; return; }
となっており、RETVAL_FALSEがZend/zend_API.hで、
#define RETVAL_FALSE ZVAL_BOOL(return_value, 0)
となっており、ZVAL_BOOLがZend/zend_API.hで、
#define ZVAL_BOOL(z, b) { \ (z)->type = IS_BOOL; \ (z)->value.lval = ((b) != 0); \ }
となっているためです。つまり、RETURN_FALSEは展開すると、
return_value->type = IS_BOOL; return_value->value.lval = (0 != 0); return;
のように、引数で与えられた戻り値用の変数に値を設定するというコードになります。結果は、
(略)
RETURN_FALSE: 8
bool(false)
(略)
になりました。どうやら、
if (dbh->methods->preparer(dbh, statement, statement_len, stmt, options TSRMLS_CC)) { pdo_stmt_construct(stmt, return_value, dbstmt_ce, ctor_args TSRMLS_CC); return; }
のdbh->methods->preparerに失敗しているようです。さて、ここでdbhとはなんでしょうか? またdbh->method->preparerはどんな関数に設定されているのでしょうか? それを突き詰めて調べることも可能ですが、現在調べているのがPDOで、PDOは各データベースドライバを隠蔽するためにあることを考えると、dbh->methods->preparerが指す関数はPDO_SQLITEにあると考えることができます。そこで、PDO_SQLITEでpreparerという名前がついている関数を探してみます。
$ cgrep preparer [~/projects/PDO_SQLITE-1.0.1] ./sqlite_driver.c:157:static int sqlite_handle_preparer(pdo_dbh_t *dbh, const char *sql, long sql_len, pdo_stmt_t *stmt, zval *driver_options TSRMLS_DC) ./sqlite_driver.c:625: sqlite_handle_preparer,
sqlite_driver.cにそれらしい関数が見付かりました。早速、デバッグ文を入れて確かにこの関数か確認してみます。
static int sqlite_handle_preparer(pdo_dbh_t *dbh, const char *sql, long sql_len, pdo_stmt_t *stmt, zval *driver_options TSRMLS_DC) { php_printf("Enter sqlite_handle_preparer(dbh=0x%x, sql=\"%s\", sql_len=%d, stmt=0x%x, driver_options=0x%x)<br>\n", dbh, sql, sql_len, stmt, driver_options);
結果は、
(略)
Enter sqlite_handle_preparer(dbh=0x84ee7bc, sql="INSERT INTO ACCOUNT (NAME, PASSWORD) VALUES (:NAME, :PASSWORD)", sql_len=62, stmt=0x84f691c, driver_options=0x0)
RETURN_FALSE: 8
(略)
となりました。確かに、sqlite_handle_preparer関数が実行されます。では各箇所にデバッグ文を挿入して挙動を確認してみましょう、としたいところですが、この関数は以下のように短いもので、エラーがsqlite3_prepare関数で発生していることはほぼ明らかです。
static int sqlite_handle_preparer(pdo_dbh_t *dbh, const char *sql, long sql_len, pdo_stmt_t *stmt, zval *driver_options TSRMLS_DC) { php_printf("Enter sqlite_handle_preparer(dbh=0x%x, sql=\"%s\", sql_len=%d, stmt=0x%x, driver_options=0x%x)<br>\n", dbh, sql, sql_len, stmt, driver_options); pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data; pdo_sqlite_stmt *S = ecalloc(1, sizeof(pdo_sqlite_stmt)); int i; const char *tail; S->H = H; stmt->driver_data = S; stmt->methods = &sqlite_stmt_methods; stmt->supports_placeholders = PDO_PLACEHOLDER_POSITIONAL|PDO_PLACEHOLDER_NAMED; if (PDO_CURSOR_FWDONLY != pdo_attr_lval(driver_options, PDO_ATTR_CURSOR, PDO_CURSOR_FWDONLY TSRMLS_CC)) { H->einfo.errcode = SQLITE_ERROR; pdo_sqlite_error(dbh); return 0; } i = sqlite3_prepare(H->db, sql, sql_len, &S->stmt, &tail); if (i == SQLITE_OK) { return 1; } pdo_sqlite_error(dbh); return 0; }
ちなみに、SQLITE_OKは、sqlite/src/sqlite.h.inで定義されていて、
#define SQLITE_OK 0 /* Successful result */
となっています。
なので、次はこのsqlite3_prepare関数を調べることにします。sqlite3_prepare関数は、PDO_SQLITEのsqlite/src/prepare.cにありました。各return文の直前にデバッグ文を入れて、動作を確かめます。
/* ** Compile the UTF-8 encoded SQL statement zSql into a statement handle. */ int sqlite3_prepare( sqlite3 *db, /* Database handle. */ const char *zSql, /* UTF-8 encoded SQL statement. */ int nBytes, /* Length of zSql in bytes. */ sqlite3_stmt **ppStmt, /* OUT: A pointer to the prepared statement */ const char** pzTail /* OUT: End of parsed string */ ){ php_printf("Enter sqlite3_prepare(db=0x%x, zSql=\"%s\", nBytes=%d)<br>\n", db, zSql, nBytes); Parse sParse; char *zErrMsg = 0; int rc = SQLITE_OK; if( sqlite3_malloc_failed ){ php_printf("return SQLITE_NOMEM<br>\n"); return SQLITE_NOMEM; } (略) if( zErrMsg ){ sqlite3Error(db, rc, "%s", zErrMsg); sqliteFree(zErrMsg); }else{ sqlite3Error(db, rc, 0); } php_printf("return rc=%d<br>\n", rc); return rc; }
結果は、
(略)
Enter sqlite3_prepare(db=0x84ee920, zSql="INSERT INTO ACCOUNT (NAME, PASSWORD) VALUES (:NAME, :PASSWORD)", nBytes=62)
Enter sqlite3_prepare(db=0x84ee920, zSql="CREATE TABLE sqlite_master( type text, name text, tbl_name text, rootpage integer, sql text )", nBytes=-1)
return rc=0
return rc=1
(略)
となりました。驚いたことに、sqlite3_prepare関数は2回呼び出されています。PDOのSQLiteドライバが、内部用のテーブルを作成しているようです。そして、そちらのSQL文は成功しています。
関数の終わり近くにエラーメッセージを検査している箇所があります。そこでエラーメッセージが設定されていないか、調べます。
if( zErrMsg ){ php_printf("zErrMsg=\"%s\"<br>\n", zErrMsg); sqlite3Error(db, rc, "%s", zErrMsg); sqliteFree(zErrMsg); }else{ sqlite3Error(db, rc, 0); } php_printf("return rc=%d<br>\n", rc); return rc; }
結果は、以下の通りでした。
(略)
zErrMsg="unsupported file format"
(略)
"unsupported file format"になっています。ファイル形式が違うという意味らしいです。なんでそうなるのか分かりません。このエラーメッセージはどこで設定されているのでしょうか?
$ cgrep "unsupported file format" [~/projects/PDO_SQLITE-1.0.1] ./sqlite/src/prepare.c:276: sqlite3SetString(pzErrMsg, "unsupported file format", (char*)0);
エラーメッセージを設定しているのは、以下の関数でした。
/* ** Attempt to read the database schema and initialize internal ** data structures for a single database file. The index of the ** database file is given by iDb. iDb==0 is used for the main ** database. iDb==1 should never be used. iDb>=2 is used for ** auxiliary databases. Return one of the SQLITE_ error codes to ** indicate success or failure. */ static int sqlite3InitOne(sqlite3 *db, int iDb, char **pzErrMsg){ int rc; BtCursor *curMain; int size; Table *pTab; char const *azArg[5]; char zDbNum[30]; int meta[10]; InitData initData; char const *zMasterSchema; char const *zMasterName = SCHEMA_TABLE(iDb); /* ** The master database table has a structure like this */ static const char master_schema = "CREATE TABLE sqlite_master(\n" " type text,\n" " name text,\n" " tbl_name text,\n" " rootpage integer,\n" " sql text\n" ")" ; #ifndef SQLITE_OMIT_TEMPDB static const char temp_master_schema = "CREATE TEMP TABLE sqlite_temp_master(\n" " type text,\n" " name text,\n" " tbl_name text,\n" " rootpage integer,\n" " sql text\n" ")" ; #else #define temp_master_schema 0 #endif assert( iDb>=0 && iDb<db->nDb ); (略) /* Get the database meta information. ** ** Meta values are as follows: ** meta[0] Schema cookie. Changes with each schema change. ** meta[1] File format of schema layer. ** meta[2] Size of the page cache. ** meta[3] Use freelist if 0. Autovacuum if greater than zero. ** meta[4] Db text encoding. 1:UTF-8 3:UTF-16 LE 4:UTF-16 BE ** meta[5] The user cookie. Used by the application. ** meta[6] ** meta[7] ** meta[8] ** meta[9] ** ** Note: The hash defined SQLITE_UTF* symbols in sqliteInt.h correspond to ** the possible values of meta[4]. */ (略) /* ** file_format==1 Version 3.0.0. ** file_format==2 Version 3.1.3. ** file_format==3 Version 3.1.4. ** ** Version 3.0 can only use files with file_format==1. Version 3.1.3 ** can read and write files with file_format==1 or file_format==2. ** Version 3.1.4 can read and write file formats 1, 2 and 3. */ php_printf("meta[1]=%d<br>\n", meta[1]); if( meta[1]>3 ){ sqlite3BtreeCloseCursor(curMain); sqlite3SetString(pzErrMsg, "unsupported file format", (char*)0); return SQLITE_ERROR; } (略) }
"CREATE TABLE sqlite_master..."というSQL文もあるので、この関数が実行されているのは間違いないようです。さて、エラーメッセージが設定されるようになる条件分岐でどのようにして処理がわかれるのか、以下のデバッグ文を挿入して調べてみます。
php_printf("meta[1]=%d<br>\n", meta[1]); if( meta[1]>3 ){ sqlite3BtreeCloseCursor(curMain); sqlite3SetString(pzErrMsg, "unsupported file format", (char*)0); return SQLITE_ERROR; }
結果は、以下の通りになりました。
(略)
meta[1]=4
(略)
ここで、エラーメッセージの前にあるコメントに注目します。
/*
** file_format==1 Version 3.0.0.
** file_format==2 Version 3.1.3.
** file_format==3 Version 3.1.4.
**
** Version 3.0 can only use files with file_format==1. Version 3.1.3
** can read and write files with file_format==1 or file_format==2.
** Version 3.1.4 can read and write file formats 1, 2 and 3.
*/
すなわち、
バージョン 3.0.0は、ファイル形式 1.
バージョン 3.1.3は、ファイル形式 2.
バージョン 3.1.4は、ファイル形式 3.バージョン3.0は、ファイル形式 1のみを扱える。バージョン 3.1.3は、ファイル形式 1または2を読み書きできる。バージョン 3.1.4は、ファイル形式1, 2と3を読み書きできる。
とのことです。ここでいうバージョンというのは、SQLiteのバージョンだと思われます。データベースを作ったSQLite 3.3.5のことについては触れられていません。SQLiteの変更記録 (http://www.sqlite.org/changes.html) には、形式の変更については触れられていませんが、PDOがSQLiteのファイル形式の変更に対応していないことがエラーの原因であると考えられます。