Bug 740218 - Support transactions in sqlitebridge and use them. r=gcp,rnewman,lucasr
authorWes Johnston <wjohnston@mozilla.com>
Mon, 09 Apr 2012 10:08:37 -0700
changeset 91260 f0f385e9f904ab2f5605ad646bcc41e55ae72a88
parent 91259 38eb61d6f1e60f59fb9fbceccabb67fefd30250e
child 91261 423e8d6463ffbede66da24a70a0ecba9ba9c3348
push idunknown
push userunknown
push dateunknown
reviewersgcp, rnewman, lucasr
bugs740218
milestone14.0a1
Bug 740218 - Support transactions in sqlitebridge and use them. r=gcp,rnewman,lucasr
mobile/android/base/db/FormHistoryProvider.java.in
mobile/android/base/db/GeckoProvider.java.in
mobile/android/base/db/PasswordsProvider.java.in
mobile/android/base/sqlite/SQLiteBridge.java
mobile/android/base/sqlite/SQLiteBridgeException.java
mozglue/android/SQLiteBridge.cpp
mozglue/android/SQLiteBridge.h
--- a/mobile/android/base/db/FormHistoryProvider.java.in
+++ b/mobile/android/base/db/FormHistoryProvider.java.in
@@ -147,27 +147,24 @@ public class FormHistoryProvider extends
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FormHistory:Init", null));
     }
 
     @Override
     public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
         if (!values.containsKey(FormHistory.GUID)) {
             return;
         }
+
         String guid = values.getAsString(FormHistory.GUID);
-        try {
-            if (guid == null) {
-                db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_NULL, null);
-                return;
-            }
-            String[] args = new String[] { guid };
-            db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_VALUE, args);
-        } catch(SQLiteBridgeException ex) {
-            Log.w(getLogTag(), "Error removing entry with GUID " + guid, ex);
+        if (guid == null) {
+            db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_NULL, null);
+            return;
         }
+        String[] args = new String[] { guid };
+        db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_VALUE, args);
      }
 
     @Override
     public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
 
     @Override
     public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
 }
--- a/mobile/android/base/db/GeckoProvider.java.in
+++ b/mobile/android/base/db/GeckoProvider.java.in
@@ -5,16 +5,18 @@
 #filter substitution
 package @ANDROID_PACKAGE_NAME@.db;
 
 import java.io.File;
 import java.io.IOException;
 import java.lang.IllegalArgumentException;
 import java.util.HashMap;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.Random;
 
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoEventListener;
 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
@@ -54,16 +56,40 @@ import android.util.Log;
 
 public abstract class GeckoProvider extends ContentProvider {
     private String mLogTag = "GeckoPasswordsProvider";
     private String mDBName = "";
     private int mDBVersion = 0;
     private HashMap<String, SQLiteBridge> mDatabasePerProfile;
     protected Context mContext = null;
 
+    @Override
+    public void shutdown() {
+        if (mDatabasePerProfile == null)
+          return;
+
+        Collection<SQLiteBridge> bridges = mDatabasePerProfile.values();
+        Iterator<SQLiteBridge> it = bridges.iterator();
+
+        while (it.hasNext()) {
+            SQLiteBridge bridge = it.next();
+            if (bridge != null) {
+                try {
+                    bridge.close();
+                } catch (Exception ex) { }
+            }
+        }
+
+        mDatabasePerProfile = null;
+    }
+
+    public void finalize() {
+        shutdown();
+    }
+
     protected void setLogTag(String aLogTag) {
         mLogTag = aLogTag;
     }
 
     protected String getLogTag() {
         return mLogTag;
     }
 
@@ -86,21 +112,24 @@ public abstract class GeckoProvider exte
     private SQLiteBridge getDB(Context context, final String databasePath) {
         SQLiteBridge bridge = null;
 
         boolean dbNeedsSetup = true;
         try {
             String resourcePath = context.getPackageResourcePath();
             GeckoAppShell.loadSQLiteLibs(context, resourcePath);
             GeckoAppShell.loadNSSLibs(context, resourcePath);
-            bridge = new SQLiteBridge(databasePath);
+            bridge = SQLiteBridge.openDatabase(databasePath, null, 0);
             int version = bridge.getVersion();
-            Log.i(mLogTag, version + " == " + mDBVersion);
             dbNeedsSetup = version != mDBVersion;
-        } catch(SQLiteBridgeException ex) {
+        } catch (SQLiteBridgeException ex) {
+            // close the database
+            if (bridge != null)
+                bridge.close();
+
             // this will throw if the database can't be found
             // we should attempt to set it up if Gecko is running
             dbNeedsSetup = true;
             Log.e(mLogTag, "Error getting version ", ex);
 
             // if Gecko is not running, we should bail out. Otherwise we try to
             // let Gecko build the database for us
             if (!GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) {
@@ -112,17 +141,18 @@ public abstract class GeckoProvider exte
         // If the database is not set up yet, or is the wrong schema version, we send an initialize
         // call to Gecko. Gecko will handle building the database file correctly, as well as any
         // migrations that are necessary
         if (dbNeedsSetup) {
             Log.i(mLogTag, "Sending init to gecko");
             bridge = null;
             initGecko();
         }
-        mDatabasePerProfile.put(databasePath, bridge);
+        if (bridge != null)
+            mDatabasePerProfile.put(databasePath, bridge);
 
         return bridge;
     }
 
     private SQLiteBridge getDatabaseForProfile(String profile) {
         if (TextUtils.isEmpty(profile)) {
             Log.d(mLogTag, "No profile provided, using default");
             profile = BrowserContract.DEFAULT_PROFILE;
@@ -199,16 +229,17 @@ public abstract class GeckoProvider exte
         final SQLiteBridge db = getDatabase(uri);
         if (db == null)
             return deleted;
 
         try {
             deleted = db.delete(getTable(uri), selection, selectionArgs);
         } catch (SQLiteBridgeException ex) {
             Log.e(mLogTag, "Error deleting record", ex);
+            throw ex;
         }
 
         return deleted;
     }
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         long id = -1;
@@ -217,45 +248,94 @@ public abstract class GeckoProvider exte
         // If we can not get a SQLiteBridge instance, its likely that the database
         // has not been set up and Gecko is not running. We return null and expect
         // callers to try again later
         if (db == null)
             return null;
 
         setupDefaults(uri, values);
 
-        onPreInsert(values, uri, db);
-
+        boolean useTransaction = !db.inTransaction();
         try {
+            if (useTransaction) {
+                db.beginTransaction();
+            }
+ 
+            // onPreInsert does a check for the item in the deleted table in some cases
+            // so we put it inside this transaction
+            onPreInsert(values, uri, db);
             id = db.insert(getTable(uri), null, values);
-        } catch(SQLiteBridgeException ex) {
+
+            if (useTransaction) {
+                db.setTransactionSuccessful();
+            }
+        } catch (SQLiteBridgeException ex) {
             Log.e(mLogTag, "Error inserting in db", ex);
+            throw ex;
+        } finally {
+            if (useTransaction) {
+                db.endTransaction();
+            }
         }
 
         return ContentUris.withAppendedId(uri, id);
     }
 
     @Override
+    public int bulkInsert(Uri uri, ContentValues[] allValues) {
+        final SQLiteBridge db = getDatabase(uri);
+        // If we can not get a SQLiteBridge instance, its likely that the database
+        // has not been set up and Gecko is not running. We return 0 and expect
+        // callers to try again later
+        if (db == null)
+            return 0;
+
+        long id = -1;
+        int rowsAdded = 0;
+
+        String table = getTable(uri);
+
+        try {
+            db.beginTransaction();
+            for (ContentValues initialValues : allValues) {
+                ContentValues values = new ContentValues(initialValues);
+                setupDefaults(uri, values);
+                onPreInsert(values, uri, db);
+                id = db.insert(table, null, values);
+                rowsAdded++;
+            }
+            db.setTransactionSuccessful();
+        } catch (SQLiteBridgeException ex) {
+            Log.e(mLogTag, "Error inserting in db", ex);
+            throw ex;
+        } finally {
+            db.endTransaction();
+        }
+        return rowsAdded;
+    }
+
+    @Override
     public int update(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         int updated = 0;
         final SQLiteBridge db = getDatabase(uri);
 
         // If we can not get a SQLiteBridge instance, its likely that the database
         // has not been set up and Gecko is not running. We return null and expect
         // callers to try again later
         if (db == null)
             return updated;
 
         onPreUpdate(values, uri, db);
 
         try {
             updated = db.update(getTable(uri), values, selection, selectionArgs);
-        } catch(SQLiteBridgeException ex) {
+        } catch (SQLiteBridgeException ex) {
             Log.e(mLogTag, "Error updating table", ex);
+            throw ex;
         }
 
         return updated;
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
@@ -270,16 +350,17 @@ public abstract class GeckoProvider exte
 
         sortOrder = getSortOrder(uri, sortOrder);
 
         try {
             cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null);
             onPostQuery(cursor, uri, db);
         } catch (SQLiteBridgeException ex) {
             Log.e(mLogTag, "Error querying database", ex);
+            throw ex;
         }
 
         return cursor;
     }
 
     public abstract String getTable(Uri uri);
 
     public abstract String getSortOrder(Uri uri, String aRequested);
--- a/mobile/android/base/db/PasswordsProvider.java.in
+++ b/mobile/android/base/db/PasswordsProvider.java.in
@@ -212,26 +212,22 @@ public class PasswordsProvider extends G
         }
         return result;
     }
 
     @Override
     public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
         if (values.containsKey(Passwords.GUID)) {
             String guid = values.getAsString(Passwords.GUID);
-            try {
-                if (guid == null) {
-                    db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null);
-                    return;
-                }
-                String[] args = new String[] { guid };
-                db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args);
-            } catch(SQLiteBridgeException ex) {
-                Log.w(getLogTag(), "Error removing entry with GUID " + guid, ex);
+            if (guid == null) {
+                db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null);
+                return;
             }
+            String[] args = new String[] { guid };
+            db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args);
         }
 
         if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
             String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
             values.put(Passwords.ENCRYPTED_PASSWORD, res);
         }
 
         if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
--- a/mobile/android/base/sqlite/SQLiteBridge.java
+++ b/mobile/android/base/sqlite/SQLiteBridge.java
@@ -3,49 +3,64 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sqlite;
 
 import org.mozilla.gecko.sqlite.SQLiteBridgeException;
 import org.mozilla.gecko.sqlite.MatrixBlobCursor;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.DatabaseErrorHandler;
 import android.text.TextUtils;
-import android.util.Log;
 
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import android.util.Log;
 import java.util.Map.Entry;
 import java.util.Set;
 
 /*
  * This class allows using the mozsqlite3 library included with Firefox
  * to read SQLite databases, instead of the Android SQLiteDataBase API,
  * which might use whatever outdated DB is present on the Android system.
  */
 public class SQLiteBridge {
     private static final String LOGTAG = "SQLiteBridge";
 
-    // Path to the database. We reopen it every query.
+    // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
     private String mDb;
+    // pointer to the database if it was opened with openDatabase
+    protected long mDbPointer = 0;
 
     // Values remembered after a query.
     private long[] mQueryResults;
 
+    private boolean mTransactionSuccess = false;
+    private boolean mInTransaction = false;
+
     private static final int RESULT_INSERT_ROW_ID = 0;
     private static final int RESULT_ROWS_CHANGED = 1;
 
     // JNI code in $(topdir)/mozglue/android/..
     private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
                                                       String[] aParams,
                                                       long[] aUpdateResult)
         throws SQLiteBridgeException;
+    private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
+                                                            String[] aParams,
+                                                            long[] aUpdateResult)
+        throws SQLiteBridgeException;
+    private static native long openDatabase(String aDb)
+        throws SQLiteBridgeException;
+    private static native void closeDatabase(long aDb);
 
     // Takes the path to the database we want to access.
     public SQLiteBridge(String aDb) throws SQLiteBridgeException {
         mDb = aDb;
     }
 
     // Executes a simple line of sql.
     public void execSQL(String sql)
@@ -199,15 +214,97 @@ public class SQLiteBridge {
         return ret;
     }
 
     // Do an SQL query, substituting the parameters in the query with the passed
     // parameters. The parameters are subsituded in order, so named parameters
     // are not supported.
     private Cursor internalQuery(String aQuery, String[] aParams)
         throws SQLiteBridgeException {
+
         mQueryResults = new long[2];
+        if (isOpen()) {
+            return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
+        }
         return sqliteCall(mDb, aQuery, aParams, mQueryResults);
     }
 
-    // nop, provided for API compatibility with SQLiteDatabase.
-    public void close() { }
+    /*
+     * The second two parameters here are just provided for compatbility with SQLiteDatabase
+     * Support for them is not currently implemented
+    */
+    public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
+        throws SQLiteException {
+        SQLiteBridge bridge = null;
+        try {
+            bridge = new SQLiteBridge(path);
+            bridge.mDbPointer = bridge.openDatabase(path);
+        } catch(SQLiteBridgeException ex) {
+            // catch and rethrow as a SQLiteException to match SQLiteDatabase
+            throw new SQLiteException(ex.getMessage());
+        }
+        return bridge;
+    }
+
+    public void close() {
+        if (isOpen()) {
+          closeDatabase(mDbPointer);
+        }
+        mDbPointer = 0;
+    }
+
+    public boolean isOpen() {
+        return mDbPointer > 0;
+    }
+
+    public void beginTransaction() throws SQLiteBridgeException {
+        if (inTransaction()) {
+            throw new SQLiteBridgeException("Nested transactions are not supported");
+        }
+        execSQL("BEGIN EXCLUSIVE");
+        mTransactionSuccess = false;
+        mInTransaction = true;
+    }
+
+    public void beginTransactionNonExclusive() throws SQLiteBridgeException {
+        if (inTransaction()) {
+            throw new SQLiteBridgeException("Nested transactions are not supported");
+        }
+        execSQL("BEGIN IMMEDIATE");
+        mTransactionSuccess = false;
+        mInTransaction = true;
+    }
+
+    public void endTransaction() {
+        if (!inTransaction())
+            return;
+
+        try {
+          if (mTransactionSuccess) {
+              execSQL("COMMIT TRANSACTION");
+          } else {
+              execSQL("ROLLBACK TRANSACTION");
+          }
+        } catch(SQLiteBridgeException ex) {
+            Log.e(LOGTAG, "Error ending transaction", ex);
+        }
+        mInTransaction = false;
+        mTransactionSuccess = false;
+    }
+
+    public void setTransactionSuccessful() throws SQLiteBridgeException {
+        if (!inTransaction()) {
+            throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
+        }
+        mTransactionSuccess = true;
+    }
+
+    public boolean inTransaction() {
+        return mInTransaction;
+    }
+
+    public void finalize() {
+        if (isOpen()) {
+            Log.e(LOGTAG, "Bridge finalized without closing the database");
+            close();
+        }
+    }
 }
--- a/mobile/android/base/sqlite/SQLiteBridgeException.java
+++ b/mobile/android/base/sqlite/SQLiteBridgeException.java
@@ -32,16 +32,16 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.sqlite;
 
-public class SQLiteBridgeException extends Exception {
+public class SQLiteBridgeException extends RuntimeException {
     static final long serialVersionUID = 1L;
 
     public SQLiteBridgeException() {}
     public SQLiteBridgeException(String msg) {
         super(msg);
     }
-}
\ No newline at end of file
+}
--- a/mozglue/android/SQLiteBridge.cpp
+++ b/mozglue/android/SQLiteBridge.cpp
@@ -163,37 +163,104 @@ extern "C" NS_EXPORT jobject JNICALL
 Java_org_mozilla_gecko_sqlite_SQLiteBridge_sqliteCall(JNIEnv* jenv, jclass,
                                                       jstring jDb,
                                                       jstring jQuery,
                                                       jobjectArray jParams,
                                                       jlongArray jQueryRes)
 {
     JNI_Setup(jenv);
 
+    int rc;
+    jobject jCursor = NULL;
+    const char* dbPath;
+    sqlite3 *db;
+    char* errorMsg;
+
+    dbPath = jenv->GetStringUTFChars(jDb, NULL);
+    rc = f_sqlite3_open(dbPath, &db);
+    jenv->ReleaseStringUTFChars(jDb, dbPath);
+    if (rc != SQLITE_OK) {
+        asprintf(&errorMsg, "Can't open database: %s\n", f_sqlite3_errmsg(db));
+        LOG("Error in SQLiteBridge: %s\n", errorMsg);
+        JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", errorMsg);
+        free(errorMsg);
+    } else {
+      jCursor = sqliteInternalCall(jenv, db, jQuery, jParams, jQueryRes);
+    }
+    f_sqlite3_close(db);
+    return jCursor;
+}
+
+extern "C" NS_EXPORT jobject JNICALL
+Java_org_mozilla_gecko_sqlite_SQLiteBridge_sqliteCallWithDb(JNIEnv* jenv, jclass,
+                                                            jlong jDb,
+                                                            jstring jQuery,
+                                                            jobjectArray jParams,
+                                                            jlongArray jQueryRes)
+{
+    JNI_Setup(jenv);
+
+    jobject jCursor = NULL;
+    sqlite3 *db = (sqlite3*)jDb;
+    jCursor = sqliteInternalCall(jenv, db, jQuery, jParams, jQueryRes);
+    return jCursor;
+}
+
+extern "C" NS_EXPORT jlong JNICALL
+Java_org_mozilla_gecko_sqlite_SQLiteBridge_openDatabase(JNIEnv* jenv, jclass,
+                                                        jstring jDb)
+{
+    JNI_Setup(jenv);
+
+    int rc;
+    const char* dbPath;
+    sqlite3 *db;
+    char* errorMsg;
+
+    dbPath = jenv->GetStringUTFChars(jDb, NULL);
+    rc = f_sqlite3_open(dbPath, &db);
+    jenv->ReleaseStringUTFChars(jDb, dbPath);
+    if (rc != SQLITE_OK) {
+        asprintf(&errorMsg, "Can't open database: %s\n", f_sqlite3_errmsg(db));
+        LOG("Error in SQLiteBridge: %s\n", errorMsg);
+        JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", errorMsg);
+        free(errorMsg);
+    }
+    return (jlong)db;
+}
+
+extern "C" NS_EXPORT void JNICALL
+Java_org_mozilla_gecko_sqlite_SQLiteBridge_closeDatabase(JNIEnv* jenv, jclass,
+                                                        jlong jDb)
+{
+    JNI_Setup(jenv);
+
+    sqlite3 *db = (sqlite3*)jDb;
+    f_sqlite3_close(db);
+}
+
+static jobject
+sqliteInternalCall(JNIEnv* jenv,
+                   sqlite3 *db,
+                   jstring jQuery,
+                   jobjectArray jParams,
+                   jlongArray jQueryRes)
+{
+    JNI_Setup(jenv);
+
     jobject jCursor = NULL;
     char* errorMsg;
     jsize numPars = 0;
 
-    const char* queryStr;
-    queryStr = jenv->GetStringUTFChars(jQuery, NULL);
-
-    const char* dbPath;
-    dbPath = jenv->GetStringUTFChars(jDb, NULL);
-
     const char *pzTail;
     sqlite3_stmt *ppStmt;
-    sqlite3 *db;
     int rc;
-    rc = f_sqlite3_open(dbPath, &db);
-    jenv->ReleaseStringUTFChars(jDb, dbPath);
 
-    if (rc != SQLITE_OK) {
-        asprintf(&errorMsg, "Can't open database: %s\n", f_sqlite3_errmsg(db));
-        goto error_close;
-    }
+    const char* queryStr;
+    queryStr = jenv->GetStringUTFChars(jQuery, NULL);
 
     rc = f_sqlite3_prepare_v2(db, queryStr, -1, &ppStmt, &pzTail);
     if (rc != SQLITE_OK || ppStmt == NULL) {
         asprintf(&errorMsg, "Can't prepare statement: %s\n", f_sqlite3_errmsg(db));
         goto error_close;
     }
     jenv->ReleaseStringUTFChars(jQuery, queryStr);
 
@@ -348,18 +415,16 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid
     }
 
     rc = f_sqlite3_finalize(ppStmt);
     if (rc != SQLITE_OK) {
         asprintf(&errorMsg, "Can't finalize statement: %s\n", f_sqlite3_errmsg(db));
         goto error_close;
     }
 
-    f_sqlite3_close(db);
     return jCursor;
 
 error_close:
-    f_sqlite3_close(db);
     LOG("Error in SQLiteBridge: %s\n", errorMsg);
     JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", errorMsg);
     free(errorMsg);
     return jCursor;
 }
--- a/mozglue/android/SQLiteBridge.h
+++ b/mozglue/android/SQLiteBridge.h
@@ -35,16 +35,17 @@
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef SQLiteBridge_h
 #define SQLiteBridge_h
 
 #include "sqlite3.h"
 
 void setup_sqlite_functions(void *sqlite_handle);
+static jobject sqliteInternalCall(JNIEnv* jenv, sqlite3 *db, jstring jQuery, jobjectArray jParams, jlongArray jQueryRes);
 
 #define SQLITE_WRAPPER(name, return_type, args...) \
 typedef return_type (*name ## _t)(args);  \
 extern name ## _t f_ ## name;
 
 SQLITE_WRAPPER(sqlite3_open, int, const char*, sqlite3**)
 SQLITE_WRAPPER(sqlite3_errmsg, const char*, sqlite3*)
 SQLITE_WRAPPER(sqlite3_prepare_v2, int, sqlite3*, const char*, int, sqlite3_stmt**, const char**)