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