Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
      4 import static android.os.Build.VERSION_CODES.LOLLIPOP;
      5 import static android.os.Build.VERSION_CODES.O;
      6 import static android.os.Build.VERSION_CODES.O_MR1;
      7 import static org.robolectric.RuntimeEnvironment.castNativePtr;
      8 
      9 import android.database.sqlite.SQLiteAbortException;
     10 import android.database.sqlite.SQLiteAccessPermException;
     11 import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException;
     12 import android.database.sqlite.SQLiteBlobTooBigException;
     13 import android.database.sqlite.SQLiteCantOpenDatabaseException;
     14 import android.database.sqlite.SQLiteConstraintException;
     15 import android.database.sqlite.SQLiteCustomFunction;
     16 import android.database.sqlite.SQLiteDatabaseCorruptException;
     17 import android.database.sqlite.SQLiteDatabaseLockedException;
     18 import android.database.sqlite.SQLiteDatatypeMismatchException;
     19 import android.database.sqlite.SQLiteDiskIOException;
     20 import android.database.sqlite.SQLiteDoneException;
     21 import android.database.sqlite.SQLiteFullException;
     22 import android.database.sqlite.SQLiteMisuseException;
     23 import android.database.sqlite.SQLiteOutOfMemoryException;
     24 import android.database.sqlite.SQLiteReadOnlyDatabaseException;
     25 import android.database.sqlite.SQLiteTableLockedException;
     26 import android.os.OperationCanceledException;
     27 import com.almworks.sqlite4java.SQLiteConnection;
     28 import com.almworks.sqlite4java.SQLiteConstants;
     29 import com.almworks.sqlite4java.SQLiteException;
     30 import com.almworks.sqlite4java.SQLiteStatement;
     31 import com.google.common.util.concurrent.Uninterruptibles;
     32 import java.io.File;
     33 import java.util.ArrayList;
     34 import java.util.Collection;
     35 import java.util.HashMap;
     36 import java.util.Map;
     37 import java.util.concurrent.Callable;
     38 import java.util.concurrent.ExecutionException;
     39 import java.util.concurrent.ExecutorService;
     40 import java.util.concurrent.Executors;
     41 import java.util.concurrent.Future;
     42 import java.util.concurrent.TimeUnit;
     43 import java.util.concurrent.atomic.AtomicBoolean;
     44 import java.util.concurrent.atomic.AtomicLong;
     45 import java.util.regex.Matcher;
     46 import java.util.regex.Pattern;
     47 import org.robolectric.annotation.Implementation;
     48 import org.robolectric.annotation.Implements;
     49 import org.robolectric.annotation.Resetter;
     50 import org.robolectric.shadows.util.SQLiteLibraryLoader;
     51 
     52 @Implements(value = android.database.sqlite.SQLiteConnection.class, isInAndroidSdk = false)
     53 public class ShadowSQLiteConnection {
     54 
     55   private static final String IN_MEMORY_PATH = ":memory:";
     56   private static final Connections CONNECTIONS = new Connections();
     57   private static final Pattern COLLATE_LOCALIZED_UNICODE_PATTERN =
     58       Pattern.compile("\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE);
     59 
     60   // indicates an ignored statement
     61   private static final int IGNORED_REINDEX_STMT = -2;
     62 
     63   private static AtomicBoolean useInMemoryDatabase = new AtomicBoolean();
     64 
     65   public static void setUseInMemoryDatabase(boolean value) {
     66     useInMemoryDatabase.set(value);
     67   }
     68 
     69   @Implementation(maxSdk = O)
     70   public static Number nativeOpen(String path, int openFlags, String label, boolean enableTrace, boolean enableProfile) {
     71     SQLiteLibraryLoader.load();
     72     return castNativePtr(CONNECTIONS.open(path));
     73   }
     74 
     75   @Implementation(minSdk = O_MR1)
     76   public static long nativeOpen(String path, int openFlags, String label, boolean enableTrace,
     77                                 boolean enableProfile, int lookasideSlotSize, int lookasideSlotCount) {
     78     return nativeOpen(path, openFlags, label, enableTrace, enableProfile).longValue();
     79   }
     80 
     81   @Implementation(maxSdk = KITKAT_WATCH)
     82   public static int nativePrepareStatement(int connectionPtr, String sql) {
     83     return (int) nativePrepareStatement((long) connectionPtr, sql);
     84   }
     85 
     86   @Implementation(minSdk = LOLLIPOP)
     87   public static long nativePrepareStatement(long connectionPtr, String sql) {
     88     final String newSql = convertSQLWithLocalizedUnicodeCollator(sql);
     89     return CONNECTIONS.prepareStatement(connectionPtr, newSql);
     90   }
     91 
     92   /**
     93    * Convert SQL with phrase COLLATE LOCALIZED or COLLATE UNICODE to COLLATE NOCASE.
     94    */
     95   static String convertSQLWithLocalizedUnicodeCollator(String sql) {
     96     Matcher matcher = COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql);
     97     return matcher.replaceAll(" COLLATE NOCASE");
     98   }
     99 
    100   @Resetter
    101   public static void reset() {
    102     CONNECTIONS.reset();
    103     useInMemoryDatabase.set(false);
    104   }
    105 
    106   @Implementation(maxSdk = KITKAT_WATCH)
    107   public static void nativeClose(int connectionPtr) {
    108     nativeClose((long) connectionPtr);
    109   }
    110 
    111   @Implementation(minSdk = LOLLIPOP)
    112   public static void nativeClose(long connectionPtr) {
    113     CONNECTIONS.close(connectionPtr);
    114   }
    115 
    116   @Implementation(maxSdk = KITKAT_WATCH)
    117   public static void nativeFinalizeStatement(int connectionPtr, int statementPtr) {
    118     nativeFinalizeStatement((long) connectionPtr, statementPtr);
    119   }
    120 
    121   @Implementation(minSdk = LOLLIPOP)
    122   public static void nativeFinalizeStatement(long connectionPtr, long statementPtr) {
    123     CONNECTIONS.finalizeStmt(connectionPtr, statementPtr);
    124   }
    125 
    126   @Implementation(maxSdk = KITKAT_WATCH)
    127   public static int nativeGetParameterCount(int connectionPtr, int statementPtr) {
    128     return nativeGetParameterCount((long) connectionPtr, statementPtr);
    129   }
    130 
    131   @Implementation(minSdk = LOLLIPOP)
    132   public static int nativeGetParameterCount(final long connectionPtr, final long statementPtr) {
    133     return CONNECTIONS.getParameterCount(connectionPtr, statementPtr);
    134   }
    135 
    136   @Implementation(maxSdk = KITKAT_WATCH)
    137   public static boolean nativeIsReadOnly(int connectionPtr, int statementPtr) {
    138     return nativeIsReadOnly((long) connectionPtr, (long) statementPtr);
    139   }
    140 
    141   @Implementation(minSdk = LOLLIPOP)
    142   public static boolean nativeIsReadOnly(final long connectionPtr, final long statementPtr) {
    143     return CONNECTIONS.isReadOnly(connectionPtr, statementPtr);
    144   }
    145 
    146   @Implementation(maxSdk = KITKAT_WATCH)
    147   public static long nativeExecuteForLong(int connectionPtr, int statementPtr) {
    148     return nativeExecuteForLong((long) connectionPtr, (long) statementPtr);
    149   }
    150 
    151   @Implementation(minSdk = LOLLIPOP)
    152   public static long nativeExecuteForLong(final long connectionPtr, final long statementPtr) {
    153     return CONNECTIONS.executeForLong(connectionPtr, statementPtr);
    154   }
    155 
    156   @Implementation(maxSdk = KITKAT_WATCH)
    157   public static void nativeExecute(int connectionPtr, int statementPtr) {
    158     nativeExecute((long) connectionPtr, (long) statementPtr);
    159   }
    160 
    161   @Implementation(minSdk = LOLLIPOP)
    162   public static void nativeExecute(final long connectionPtr, final long statementPtr) {
    163     CONNECTIONS.executeStatement(connectionPtr, statementPtr);
    164   }
    165 
    166   @Implementation(maxSdk = KITKAT_WATCH)
    167   public static String nativeExecuteForString(int connectionPtr, int statementPtr) {
    168     return nativeExecuteForString((long) connectionPtr, (long) statementPtr);
    169   }
    170 
    171   @Implementation(minSdk = LOLLIPOP)
    172   public static String nativeExecuteForString(final long connectionPtr, final long statementPtr) {
    173     return CONNECTIONS.executeForString(connectionPtr, statementPtr);
    174   }
    175 
    176   @Implementation(maxSdk = KITKAT_WATCH)
    177   public static int nativeGetColumnCount(int connectionPtr, int statementPtr) {
    178     return nativeGetColumnCount((long) connectionPtr, (long) statementPtr);
    179   }
    180 
    181   @Implementation(minSdk = LOLLIPOP)
    182   public static int nativeGetColumnCount(final long connectionPtr, final long statementPtr) {
    183     return CONNECTIONS.getColumnCount(connectionPtr, statementPtr);
    184   }
    185 
    186   @Implementation(maxSdk = KITKAT_WATCH)
    187   public static String nativeGetColumnName(int connectionPtr, int statementPtr, int index) {
    188     return nativeGetColumnName((long) connectionPtr, (long) statementPtr, index);
    189   }
    190 
    191   @Implementation(minSdk = LOLLIPOP)
    192   public static String nativeGetColumnName(final long connectionPtr, final long statementPtr, final int index) {
    193     return CONNECTIONS.getColumnName(connectionPtr, statementPtr, index);
    194   }
    195 
    196   @Implementation(maxSdk = KITKAT_WATCH)
    197   public static void nativeBindNull(int connectionPtr, int statementPtr, int index) {
    198     nativeBindNull((long) connectionPtr, (long) statementPtr, index);
    199   }
    200 
    201   @Implementation(minSdk = LOLLIPOP)
    202   public static void nativeBindNull(final long connectionPtr, final long statementPtr, final int index) {
    203     CONNECTIONS.bindNull(connectionPtr, statementPtr, index);
    204   }
    205 
    206   @Implementation(maxSdk = KITKAT_WATCH)
    207   public static void nativeBindLong(int connectionPtr, int statementPtr, int index, long value) {
    208     nativeBindLong((long) connectionPtr, (long) statementPtr, index, value);
    209   }
    210 
    211   @Implementation(minSdk = LOLLIPOP)
    212   public static void nativeBindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
    213     CONNECTIONS.bindLong(connectionPtr, statementPtr, index, value);
    214   }
    215 
    216   @Implementation(maxSdk = KITKAT_WATCH)
    217   public static void nativeBindDouble(int connectionPtr, int statementPtr, int index, double value) {
    218     nativeBindDouble((long) connectionPtr, (long) statementPtr, index, value);
    219   }
    220 
    221   @Implementation(minSdk = LOLLIPOP)
    222   public static void nativeBindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
    223     CONNECTIONS.bindDouble(connectionPtr, statementPtr, index, value);
    224   }
    225 
    226   @Implementation(maxSdk = KITKAT_WATCH)
    227   public static void nativeBindString(int connectionPtr, int statementPtr, int index, String value) {
    228     nativeBindString((long) connectionPtr, (long) statementPtr, index, value);
    229   }
    230 
    231   @Implementation(minSdk = LOLLIPOP)
    232   public static void nativeBindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
    233     CONNECTIONS.bindString(connectionPtr, statementPtr, index, value);
    234   }
    235 
    236   @Implementation(maxSdk = KITKAT_WATCH)
    237   public static void nativeBindBlob(int connectionPtr, int statementPtr, int index, byte[] value) {
    238     nativeBindBlob((long) connectionPtr, (long) statementPtr, index, value);
    239   }
    240 
    241   @Implementation(minSdk = LOLLIPOP)
    242   public static void nativeBindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
    243     CONNECTIONS.bindBlob(connectionPtr, statementPtr, index, value);
    244   }
    245 
    246   @Implementation(maxSdk = KITKAT_WATCH)
    247   public static void nativeRegisterLocalizedCollators(int connectionPtr, String locale) {
    248     nativeRegisterLocalizedCollators((long) connectionPtr, locale);
    249   }
    250 
    251   @Implementation(minSdk = LOLLIPOP)
    252   public static void nativeRegisterLocalizedCollators(long connectionPtr, String locale) {
    253     // TODO: find a way to create a collator
    254     // http://www.sqlite.org/c3ref/create_collation.html
    255     // xerial jdbc driver does not have a Java method for sqlite3_create_collation
    256   }
    257 
    258   @Implementation(maxSdk = KITKAT_WATCH)
    259   public static int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr) {
    260     return nativeExecuteForChangedRowCount((long) connectionPtr, (long) statementPtr);
    261   }
    262 
    263   @Implementation(minSdk = LOLLIPOP)
    264   public static int nativeExecuteForChangedRowCount(final long connectionPtr, final long statementPtr) {
    265     return CONNECTIONS.executeForChangedRowCount(connectionPtr, statementPtr);
    266   }
    267 
    268   @Implementation(maxSdk = KITKAT_WATCH)
    269   public static long nativeExecuteForLastInsertedRowId(int connectionPtr, int statementPtr) {
    270     return nativeExecuteForLastInsertedRowId((long) connectionPtr, (long) statementPtr);
    271   }
    272 
    273   @Implementation(minSdk = LOLLIPOP)
    274   public static long nativeExecuteForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
    275     return CONNECTIONS.executeForLastInsertedRowId(connectionPtr, statementPtr);
    276   }
    277 
    278   @Implementation(maxSdk = KITKAT_WATCH)
    279   public static long nativeExecuteForCursorWindow(int connectionPtr, int statementPtr, int windowPtr,
    280                                                   int startPos, int requiredPos, boolean countAllRows) {
    281     return nativeExecuteForCursorWindow((long) connectionPtr, (long) statementPtr, (long) windowPtr,
    282         startPos, requiredPos, countAllRows);
    283 }
    284 
    285   @Implementation(minSdk = LOLLIPOP)
    286   public static long nativeExecuteForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr,
    287                                                   final int startPos, final int requiredPos, final boolean countAllRows) {
    288     return CONNECTIONS.executeForCursorWindow(connectionPtr, statementPtr, windowPtr);
    289   }
    290 
    291   @Implementation(maxSdk = KITKAT_WATCH)
    292   public static void nativeResetStatementAndClearBindings(int connectionPtr, int statementPtr) {
    293     nativeResetStatementAndClearBindings((long) connectionPtr, (long) statementPtr);
    294   }
    295 
    296   @Implementation(minSdk = LOLLIPOP)
    297   public static void nativeResetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
    298     CONNECTIONS.resetStatementAndClearBindings(connectionPtr, statementPtr);
    299   }
    300 
    301   @Implementation(maxSdk = KITKAT_WATCH)
    302   public static void nativeCancel(int connectionPtr) {
    303     nativeCancel((long) connectionPtr);
    304   }
    305 
    306   @Implementation(minSdk = LOLLIPOP)
    307   public static void nativeCancel(long connectionPtr) {
    308     CONNECTIONS.cancel(connectionPtr);
    309   }
    310 
    311   @Implementation(maxSdk = KITKAT_WATCH)
    312   public static void nativeResetCancel(int connectionPtr, boolean cancelable) {
    313     nativeResetCancel((long) connectionPtr, cancelable);
    314   }
    315 
    316   @Implementation(minSdk = LOLLIPOP)
    317   public static void nativeResetCancel(long connectionPtr, boolean cancelable) {
    318     // handled in com.almworks.sqlite4java.SQLiteConnection#exec
    319   }
    320 
    321   @Implementation(maxSdk = KITKAT_WATCH)
    322   public static void nativeRegisterCustomFunction(int connectionPtr, SQLiteCustomFunction function) {
    323     nativeRegisterCustomFunction((long) connectionPtr, function);
    324   }
    325 
    326   @Implementation(minSdk = LOLLIPOP)
    327   public static void nativeRegisterCustomFunction(long connectionPtr, SQLiteCustomFunction function) {
    328     // not supported
    329   }
    330 
    331   @Implementation (maxSdk = KITKAT_WATCH)
    332   public static int nativeExecuteForBlobFileDescriptor(int connectionPtr, int statementPtr) {
    333     return nativeExecuteForBlobFileDescriptor((long) connectionPtr, (long) statementPtr);
    334   }
    335 
    336   @Implementation(minSdk = LOLLIPOP)
    337   public static int nativeExecuteForBlobFileDescriptor(long connectionPtr, long statementPtr) {
    338     // impossible to support without native code?
    339     return -1;
    340   }
    341 
    342   @Implementation(maxSdk = KITKAT_WATCH)
    343   public static int nativeGetDbLookaside(int connectionPtr) {
    344     return nativeGetDbLookaside((long) connectionPtr);
    345   }
    346 
    347   @Implementation(minSdk = LOLLIPOP)
    348   public static int nativeGetDbLookaside(long connectionPtr) {
    349     // not supported by sqlite4java
    350     return 0;
    351   }
    352 // VisibleForTesting
    353 static class Connections {
    354 
    355   private final Object lock = new Object();
    356   private final AtomicLong pointerCounter = new AtomicLong(0);
    357   private final Map<Long, SQLiteStatement> statementsMap = new HashMap<>();
    358   private final Map<Long, SQLiteConnection> connectionsMap = new HashMap<>();
    359 
    360   private ExecutorService dbExecutor = Executors.newSingleThreadExecutor();
    361 
    362   SQLiteConnection getConnection(final long connectionPtr) {
    363     synchronized (lock) {
    364       final SQLiteConnection connection = connectionsMap.get(connectionPtr);
    365       if (connection == null) {
    366         throw new IllegalStateException("Illegal connection pointer " + connectionPtr
    367                 + ". Current pointers for thread " + Thread.currentThread() + " " + connectionsMap.keySet());
    368       }
    369       return connection;
    370     }
    371   }
    372 
    373   SQLiteStatement getStatement(final long connectionPtr, final long statementPtr) {
    374     synchronized (lock) {
    375       // ensure connection is ok
    376       getConnection(connectionPtr);
    377 
    378       final SQLiteStatement statement = statementsMap.get(statementPtr);
    379       if (statement == null) {
    380         throw new IllegalArgumentException("Invalid prepared statement pointer: " + statementPtr + ". Current pointers: " + statementsMap.keySet());
    381       }
    382       if (statement.isDisposed()) {
    383         throw new IllegalStateException("Statement " + statementPtr + " " + statement + " is disposed");
    384       }
    385       return statement;
    386     }
    387   }
    388 
    389   long open(final String path) {
    390     synchronized (lock) {
    391       final SQLiteConnection dbConnection = execute("open SQLite connection", new Callable<SQLiteConnection>() {
    392         @Override
    393         public SQLiteConnection call() throws Exception {
    394           SQLiteConnection connection = useInMemoryDatabase.get() || IN_MEMORY_PATH.equals(path)
    395                   ? new SQLiteConnection()
    396                   : new SQLiteConnection(new File(path));
    397 
    398           connection.open();
    399           return connection;
    400         }
    401       });
    402 
    403       final long connectionPtr = pointerCounter.incrementAndGet();
    404       connectionsMap.put(connectionPtr, dbConnection);
    405       return connectionPtr;
    406     }
    407   }
    408 
    409   long prepareStatement(final long connectionPtr, final String sql) {
    410     // TODO: find a way to create collators
    411     if ("REINDEX LOCALIZED".equals(sql)) {
    412       return IGNORED_REINDEX_STMT;
    413     }
    414 
    415     synchronized (lock) {
    416       final SQLiteConnection connection = getConnection(connectionPtr);
    417       final SQLiteStatement statement = execute("prepare statement", new Callable<SQLiteStatement>() {
    418         @Override
    419         public SQLiteStatement call() throws Exception {
    420           return connection.prepare(sql);
    421         }
    422       });
    423 
    424       final long statementPtr = pointerCounter.incrementAndGet();
    425       statementsMap.put(statementPtr, statement);
    426       return statementPtr;
    427     }
    428   }
    429 
    430   void close(final long connectionPtr) {
    431     synchronized (lock) {
    432       final SQLiteConnection connection = getConnection(connectionPtr);
    433         execute("close connection", new Callable<Void>() {
    434         @Override
    435         public Void call() throws Exception {
    436           connection.dispose();
    437           return null;
    438         }
    439       });
    440       connectionsMap.remove(connectionPtr);
    441     }
    442   }
    443 
    444   void reset() {
    445     ExecutorService oldDbExecutor;
    446     Collection<SQLiteConnection> openConnections;
    447 
    448     synchronized (lock) {
    449       oldDbExecutor = dbExecutor;
    450       openConnections = new ArrayList<>(connectionsMap.values());
    451 
    452       dbExecutor = Executors.newSingleThreadExecutor();
    453       connectionsMap.clear();
    454       statementsMap.clear();
    455     }
    456 
    457     shutdownDbExecutor(oldDbExecutor, openConnections);
    458   }
    459 
    460   private static void shutdownDbExecutor(ExecutorService executorService, Collection<SQLiteConnection> connections) {
    461     for (final SQLiteConnection connection : connections) {
    462       getFuture("close connection on reset", executorService.submit(new Callable<Void>() {
    463         @Override
    464         public Void call() throws Exception {
    465           connection.dispose();
    466           return null;
    467         }
    468       }));
    469     }
    470 
    471     executorService.shutdown();
    472     try {
    473       executorService.awaitTermination(30, TimeUnit.SECONDS);
    474     } catch (InterruptedException e) {
    475       throw new RuntimeException(e);
    476     }
    477   }
    478 
    479   void finalizeStmt(final long connectionPtr, final long statementPtr) {
    480     if (statementPtr == IGNORED_REINDEX_STMT) {
    481       return;
    482     }
    483 
    484     synchronized (lock) {
    485       final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
    486       statementsMap.remove(statementPtr);
    487 
    488         execute("finalize statement", new Callable<Void>() {
    489         @Override
    490         public Void call() throws Exception {
    491           statement.dispose();
    492           return null;
    493         }
    494       });
    495     }
    496   }
    497 
    498   void cancel(final long connectionPtr) {
    499     synchronized (lock) {
    500       getConnection(connectionPtr); // check connection
    501 
    502       final SQLiteStatement statement = statementsMap.get(pointerCounter.get());
    503       if (statement != null) {
    504           execute("cancel", new Callable<Void>() {
    505           @Override
    506           public Void call() throws Exception {
    507             statement.cancel();
    508             return null;
    509           }
    510         });
    511       }
    512     }
    513   }
    514 
    515   int getParameterCount(final long connectionPtr, final long statementPtr) {
    516     if (statementPtr == IGNORED_REINDEX_STMT) {
    517       return 0;
    518     }
    519 
    520     return executeStatementOperation(connectionPtr, statementPtr, "get parameters count in prepared statement", new StatementOperation<Integer>() {
    521       @Override
    522       public Integer call(final SQLiteStatement statement) throws Exception {
    523         return statement.getBindParameterCount();
    524       }
    525     });
    526   }
    527 
    528   boolean isReadOnly(final long connectionPtr, final long statementPtr) {
    529     if (statementPtr == IGNORED_REINDEX_STMT) {
    530       return true;
    531     }
    532 
    533     return executeStatementOperation(connectionPtr, statementPtr, "call isReadOnly", new StatementOperation<Boolean>() {
    534       @Override
    535       public Boolean call(final SQLiteStatement statement) throws Exception {
    536         return statement.isReadOnly();
    537       }
    538     });
    539   }
    540 
    541   long executeForLong(final long connectionPtr, final long statementPtr) {
    542     return executeStatementOperation(connectionPtr, statementPtr, "execute for long", new StatementOperation<Long>() {
    543       @Override
    544       public Long call(final SQLiteStatement statement) throws Exception {
    545         if (!statement.step()) {
    546           throw new SQLiteException(SQLiteConstants.SQLITE_DONE, "No rows returned from query");
    547         }
    548         return statement.columnLong(0);
    549       }
    550     });
    551   }
    552 
    553   void executeStatement(final long connectionPtr, final long statementPtr) {
    554     if (statementPtr == IGNORED_REINDEX_STMT) {
    555       return;
    556     }
    557 
    558     executeStatementOperation(connectionPtr, statementPtr, "execute", new StatementOperation<Void>() {
    559       @Override
    560       public Void call(final SQLiteStatement statement) throws Exception {
    561         statement.stepThrough();
    562         return null;
    563       }
    564     });
    565   }
    566 
    567   String executeForString(final long connectionPtr, final long statementPtr) {
    568     return executeStatementOperation(connectionPtr, statementPtr, "execute for string", new StatementOperation<String>() {
    569       @Override
    570       public String call(final SQLiteStatement statement) throws Exception {
    571         if (!statement.step()) {
    572           throw new SQLiteException(SQLiteConstants.SQLITE_DONE, "No rows returned from query");
    573         }
    574         return statement.columnString(0);
    575       }
    576     });
    577   }
    578 
    579   int getColumnCount(final long connectionPtr, final long statementPtr) {
    580     return executeStatementOperation(connectionPtr, statementPtr, "get columns count", new StatementOperation<Integer>() {
    581       @Override
    582       public Integer call(final SQLiteStatement statement) throws Exception {
    583         return statement.columnCount();
    584       }
    585     });
    586   }
    587 
    588   String getColumnName(final long connectionPtr, final long statementPtr, final int index) {
    589     return executeStatementOperation(connectionPtr, statementPtr, "get column name at index " + index, new StatementOperation<String>() {
    590       @Override
    591       public String call(final SQLiteStatement statement) throws Exception {
    592         return statement.getColumnName(index);
    593       }
    594     });
    595   }
    596 
    597   void bindNull(final long connectionPtr, final long statementPtr, final int index) {
    598     executeStatementOperation(connectionPtr, statementPtr, "bind null at index " + index, new StatementOperation<Void>() {
    599       @Override
    600       public Void call(final SQLiteStatement statement) throws Exception {
    601         statement.bindNull(index);
    602         return null;
    603       }
    604     });
    605   }
    606 
    607   void bindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
    608     executeStatementOperation(connectionPtr, statementPtr, "bind long at index " + index + " with value " + value, new StatementOperation<Void>() {
    609       @Override
    610       public Void call(final SQLiteStatement statement) throws Exception {
    611         statement.bind(index, value);
    612         return null;
    613       }
    614     });
    615   }
    616 
    617   void bindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
    618     executeStatementOperation(connectionPtr, statementPtr, "bind double at index " + index + " with value " + value, new StatementOperation<Void>() {
    619       @Override
    620       public Void call(final SQLiteStatement statement) throws Exception {
    621         statement.bind(index, value);
    622         return null;
    623       }
    624     });
    625   }
    626 
    627   void bindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
    628     executeStatementOperation(connectionPtr, statementPtr, "bind string at index " + index, new StatementOperation<Void>() {
    629       @Override
    630       public Void call(final SQLiteStatement statement) throws Exception {
    631         statement.bind(index, value);
    632         return null;
    633       }
    634     });
    635   }
    636 
    637   void bindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
    638     executeStatementOperation(connectionPtr, statementPtr, "bind blob at index " + index, new StatementOperation<Void>() {
    639       @Override
    640       public Void call(final SQLiteStatement statement) throws Exception {
    641         statement.bind(index, value);
    642         return null;
    643       }
    644     });
    645   }
    646 
    647   int executeForChangedRowCount(final long connectionPtr, final long statementPtr) {
    648     synchronized (lock) {
    649       final SQLiteConnection connection = getConnection(connectionPtr);
    650       final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
    651 
    652       return execute("execute for changed row count", new Callable<Integer>() {
    653         @Override
    654         public Integer call() throws Exception {
    655           statement.stepThrough();
    656           return connection.getChanges();
    657         }
    658       });
    659     }
    660   }
    661 
    662   long executeForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
    663     synchronized (lock) {
    664       final SQLiteConnection connection = getConnection(connectionPtr);
    665       final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
    666 
    667       return execute("execute for last inserted row ID", new Callable<Long>() {
    668         @Override
    669         public Long call() throws Exception {
    670           statement.stepThrough();
    671           return connection.getLastInsertId();
    672         }
    673       });
    674     }
    675   }
    676 
    677   long executeForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr) {
    678     return executeStatementOperation(connectionPtr, statementPtr, "execute for cursor window", new StatementOperation<Integer>() {
    679       @Override
    680       public Integer call(final SQLiteStatement statement) throws Exception {
    681         return ShadowCursorWindow.setData(windowPtr, statement);
    682       }
    683     });
    684   }
    685 
    686   void resetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
    687     executeStatementOperation(connectionPtr, statementPtr, "reset statement", new StatementOperation<Void>() {
    688       @Override
    689       public Void call(final SQLiteStatement statement) throws Exception {
    690         statement.reset(true);
    691         return null;
    692       }
    693     });
    694   }
    695 
    696   interface StatementOperation<T> {
    697     T call(final SQLiteStatement statement) throws Exception;
    698   }
    699 
    700   private <T> T executeStatementOperation(final long connectionPtr,
    701                                           final long statementPtr,
    702                                           final String comment,
    703                                           final StatementOperation<T> statementOperation) {
    704     synchronized (lock) {
    705       final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
    706       return execute(comment, new Callable<T>() {
    707         @Override
    708         public T call() throws Exception {
    709           return statementOperation.call(statement);
    710         }
    711       });
    712     }
    713   }
    714 
    715   /**
    716    * Any Callable passed in to execute must not synchronize on lock, as this will result in a deadlock
    717    */
    718   private <T> T execute(final String comment, final Callable<T> work) {
    719     synchronized (lock) {
    720       return getFuture(comment, dbExecutor.submit(work));
    721     }
    722   }
    723 
    724   private static <T> T getFuture(final String comment, final Future<T> future) {
    725     try {
    726       return Uninterruptibles.getUninterruptibly(future);
    727       // No need to catch cancellationexception - we never cancel these futures
    728     } catch (ExecutionException e) {
    729       Throwable t = e.getCause();
    730       if (t instanceof SQLiteException) {
    731         final RuntimeException sqlException = getSqliteException("Cannot " + comment, ((SQLiteException) t).getBaseErrorCode());
    732         sqlException.initCause(e);
    733         throw sqlException;
    734       } else {
    735         throw new RuntimeException(e);
    736       }
    737     }
    738   }
    739 
    740   private static RuntimeException getSqliteException(final String message, final int baseErrorCode) {
    741     // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
    742     switch (baseErrorCode) {
    743       case SQLiteConstants.SQLITE_ABORT: return new SQLiteAbortException(message);
    744       case SQLiteConstants.SQLITE_PERM: return new SQLiteAccessPermException(message);
    745       case SQLiteConstants.SQLITE_RANGE: return new SQLiteBindOrColumnIndexOutOfRangeException(message);
    746       case SQLiteConstants.SQLITE_TOOBIG: return new SQLiteBlobTooBigException(message);
    747       case SQLiteConstants.SQLITE_CANTOPEN: return new SQLiteCantOpenDatabaseException(message);
    748       case SQLiteConstants.SQLITE_CONSTRAINT: return new SQLiteConstraintException(message);
    749       case SQLiteConstants.SQLITE_NOTADB: // fall through
    750       case SQLiteConstants.SQLITE_CORRUPT: return new SQLiteDatabaseCorruptException(message);
    751       case SQLiteConstants.SQLITE_BUSY: return new SQLiteDatabaseLockedException(message);
    752       case SQLiteConstants.SQLITE_MISMATCH: return new SQLiteDatatypeMismatchException(message);
    753       case SQLiteConstants.SQLITE_IOERR: return new SQLiteDiskIOException(message);
    754       case SQLiteConstants.SQLITE_DONE: return new SQLiteDoneException(message);
    755       case SQLiteConstants.SQLITE_FULL: return new SQLiteFullException(message);
    756       case SQLiteConstants.SQLITE_MISUSE: return new SQLiteMisuseException(message);
    757       case SQLiteConstants.SQLITE_NOMEM: return new SQLiteOutOfMemoryException(message);
    758       case SQLiteConstants.SQLITE_READONLY: return new SQLiteReadOnlyDatabaseException(message);
    759       case SQLiteConstants.SQLITE_LOCKED: return new SQLiteTableLockedException(message);
    760       case SQLiteConstants.SQLITE_INTERRUPT: return new OperationCanceledException(message);
    761       default: return new android.database.sqlite.SQLiteException(message
    762           + ", base error code: " + baseErrorCode);
    763     }
    764   }
    765 }
    766 }
    767