All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowSQLiteConnection Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static org.robolectric.RuntimeEnvironment.castNativePtr;

import android.database.sqlite.SQLiteAbortException;
import android.database.sqlite.SQLiteAccessPermException;
import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException;
import android.database.sqlite.SQLiteBlobTooBigException;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteCustomFunction;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDatabaseLockedException;
import android.database.sqlite.SQLiteDatatypeMismatchException;
import android.database.sqlite.SQLiteDiskIOException;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteMisuseException;
import android.database.sqlite.SQLiteOutOfMemoryException;
import android.database.sqlite.SQLiteReadOnlyDatabaseException;
import android.database.sqlite.SQLiteTableLockedException;
import android.os.OperationCanceledException;
import com.almworks.sqlite4java.SQLiteConnection;
import com.almworks.sqlite4java.SQLiteConstants;
import com.almworks.sqlite4java.SQLiteException;
import com.almworks.sqlite4java.SQLiteStatement;
import com.google.common.util.concurrent.Uninterruptibles;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadows.util.SQLiteLibraryLoader;

@Implements(value = android.database.sqlite.SQLiteConnection.class, isInAndroidSdk = false)
public class ShadowSQLiteConnection {

  private static final String IN_MEMORY_PATH = ":memory:";
  private static final Connections CONNECTIONS = new Connections();
  private static final Pattern COLLATE_LOCALIZED_UNICODE_PATTERN =
      Pattern.compile("\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE);

  // indicates an ignored statement
  private static final int IGNORED_REINDEX_STMT = -2;

  private static AtomicBoolean useInMemoryDatabase = new AtomicBoolean();

  public static void setUseInMemoryDatabase(boolean value) {
    useInMemoryDatabase.set(value);
  }

  @Implementation(maxSdk = O)
  public static Number nativeOpen(String path, int openFlags, String label, boolean enableTrace, boolean enableProfile) {
    SQLiteLibraryLoader.load();
    return castNativePtr(CONNECTIONS.open(path));
  }

  @Implementation(minSdk = O_MR1)
  public static long nativeOpen(String path, int openFlags, String label, boolean enableTrace,
                                boolean enableProfile, int lookasideSlotSize, int lookasideSlotCount) {
    return nativeOpen(path, openFlags, label, enableTrace, enableProfile).longValue();
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativePrepareStatement(int connectionPtr, String sql) {
    return (int) nativePrepareStatement((long) connectionPtr, sql);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static long nativePrepareStatement(long connectionPtr, String sql) {
    final String newSql = convertSQLWithLocalizedUnicodeCollator(sql);
    return CONNECTIONS.prepareStatement(connectionPtr, newSql);
  }

  /**
   * Convert SQL with phrase COLLATE LOCALIZED or COLLATE UNICODE to COLLATE NOCASE.
   */
  static String convertSQLWithLocalizedUnicodeCollator(String sql) {
    Matcher matcher = COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql);
    return matcher.replaceAll(" COLLATE NOCASE");
  }

  @Resetter
  public static void reset() {
    CONNECTIONS.reset();
    useInMemoryDatabase.set(false);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeClose(int connectionPtr) {
    nativeClose((long) connectionPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeClose(long connectionPtr) {
    CONNECTIONS.close(connectionPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeFinalizeStatement(int connectionPtr, int statementPtr) {
    nativeFinalizeStatement((long) connectionPtr, statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeFinalizeStatement(long connectionPtr, long statementPtr) {
    CONNECTIONS.finalizeStmt(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeGetParameterCount(int connectionPtr, int statementPtr) {
    return nativeGetParameterCount((long) connectionPtr, statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeGetParameterCount(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.getParameterCount(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static boolean nativeIsReadOnly(int connectionPtr, int statementPtr) {
    return nativeIsReadOnly((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static boolean nativeIsReadOnly(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.isReadOnly(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeExecuteForLong(int connectionPtr, int statementPtr) {
    return nativeExecuteForLong((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static long nativeExecuteForLong(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForLong(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeExecute(int connectionPtr, int statementPtr) {
    nativeExecute((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeExecute(final long connectionPtr, final long statementPtr) {
    CONNECTIONS.executeStatement(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static String nativeExecuteForString(int connectionPtr, int statementPtr) {
    return nativeExecuteForString((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static String nativeExecuteForString(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForString(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeGetColumnCount(int connectionPtr, int statementPtr) {
    return nativeGetColumnCount((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeGetColumnCount(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.getColumnCount(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static String nativeGetColumnName(int connectionPtr, int statementPtr, int index) {
    return nativeGetColumnName((long) connectionPtr, (long) statementPtr, index);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static String nativeGetColumnName(final long connectionPtr, final long statementPtr, final int index) {
    return CONNECTIONS.getColumnName(connectionPtr, statementPtr, index);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindNull(int connectionPtr, int statementPtr, int index) {
    nativeBindNull((long) connectionPtr, (long) statementPtr, index);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindNull(final long connectionPtr, final long statementPtr, final int index) {
    CONNECTIONS.bindNull(connectionPtr, statementPtr, index);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindLong(int connectionPtr, int statementPtr, int index, long value) {
    nativeBindLong((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
    CONNECTIONS.bindLong(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindDouble(int connectionPtr, int statementPtr, int index, double value) {
    nativeBindDouble((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
    CONNECTIONS.bindDouble(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindString(int connectionPtr, int statementPtr, int index, String value) {
    nativeBindString((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
    CONNECTIONS.bindString(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindBlob(int connectionPtr, int statementPtr, int index, byte[] value) {
    nativeBindBlob((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
    CONNECTIONS.bindBlob(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeRegisterLocalizedCollators(int connectionPtr, String locale) {
    nativeRegisterLocalizedCollators((long) connectionPtr, locale);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeRegisterLocalizedCollators(long connectionPtr, String locale) {
    // TODO: find a way to create a collator
    // http://www.sqlite.org/c3ref/create_collation.html
    // xerial jdbc driver does not have a Java method for sqlite3_create_collation
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr) {
    return nativeExecuteForChangedRowCount((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeExecuteForChangedRowCount(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForChangedRowCount(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeExecuteForLastInsertedRowId(int connectionPtr, int statementPtr) {
    return nativeExecuteForLastInsertedRowId((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static long nativeExecuteForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForLastInsertedRowId(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeExecuteForCursorWindow(int connectionPtr, int statementPtr, int windowPtr,
                                                  int startPos, int requiredPos, boolean countAllRows) {
    return nativeExecuteForCursorWindow((long) connectionPtr, (long) statementPtr, (long) windowPtr,
        startPos, requiredPos, countAllRows);
}

  @Implementation(minSdk = LOLLIPOP)
  public static long nativeExecuteForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr,
                                                  final int startPos, final int requiredPos, final boolean countAllRows) {
    return CONNECTIONS.executeForCursorWindow(connectionPtr, statementPtr, windowPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeResetStatementAndClearBindings(int connectionPtr, int statementPtr) {
    nativeResetStatementAndClearBindings((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeResetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
    CONNECTIONS.resetStatementAndClearBindings(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeCancel(int connectionPtr) {
    nativeCancel((long) connectionPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeCancel(long connectionPtr) {
    CONNECTIONS.cancel(connectionPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeResetCancel(int connectionPtr, boolean cancelable) {
    nativeResetCancel((long) connectionPtr, cancelable);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeResetCancel(long connectionPtr, boolean cancelable) {
    // handled in com.almworks.sqlite4java.SQLiteConnection#exec
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeRegisterCustomFunction(int connectionPtr, SQLiteCustomFunction function) {
    nativeRegisterCustomFunction((long) connectionPtr, function);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeRegisterCustomFunction(long connectionPtr, SQLiteCustomFunction function) {
    // not supported
  }

  @Implementation (maxSdk = KITKAT_WATCH)
  public static int nativeExecuteForBlobFileDescriptor(int connectionPtr, int statementPtr) {
    return nativeExecuteForBlobFileDescriptor((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeExecuteForBlobFileDescriptor(long connectionPtr, long statementPtr) {
    // impossible to support without native code?
    return -1;
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeGetDbLookaside(int connectionPtr) {
    return nativeGetDbLookaside((long) connectionPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeGetDbLookaside(long connectionPtr) {
    // not supported by sqlite4java
    return 0;
  }
// VisibleForTesting
static class Connections {

  private final Object lock = new Object();
  private final AtomicLong pointerCounter = new AtomicLong(0);
  private final Map statementsMap = new HashMap<>();
  private final Map connectionsMap = new HashMap<>();
  private final Map> statementPtrsForConnection = new HashMap<>();

  private ExecutorService dbExecutor = Executors.newSingleThreadExecutor();

  SQLiteConnection getConnection(final long connectionPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = connectionsMap.get(connectionPtr);
      if (connection == null) {
        throw new IllegalStateException("Illegal connection pointer " + connectionPtr
                + ". Current pointers for thread " + Thread.currentThread() + " " + connectionsMap.keySet());
      }
      return connection;
    }
  }

  SQLiteStatement getStatement(final long connectionPtr, final long statementPtr) {
    synchronized (lock) {
      // ensure connection is ok
      getConnection(connectionPtr);

      final SQLiteStatement statement = statementsMap.get(statementPtr);
      if (statement == null) {
        throw new IllegalArgumentException("Invalid prepared statement pointer: " + statementPtr + ". Current pointers: " + statementsMap.keySet());
      }
      if (statement.isDisposed()) {
        throw new IllegalStateException("Statement " + statementPtr + " " + statement + " is disposed");
      }
      return statement;
    }
  }

  long open(final String path) {
    synchronized (lock) {
      final SQLiteConnection dbConnection = execute("open SQLite connection", new Callable() {
        @Override
        public SQLiteConnection call() throws Exception {
          SQLiteConnection connection = useInMemoryDatabase.get() || IN_MEMORY_PATH.equals(path)
                  ? new SQLiteConnection()
                  : new SQLiteConnection(new File(path));

          connection.open();
          return connection;
        }
      });

      final long connectionPtr = pointerCounter.incrementAndGet();
      connectionsMap.put(connectionPtr, dbConnection);
      statementPtrsForConnection.put(connectionPtr, new ArrayList<>());
      return connectionPtr;
    }
  }

  long prepareStatement(final long connectionPtr, final String sql) {
    // TODO: find a way to create collators
    if ("REINDEX LOCALIZED".equals(sql)) {
      return IGNORED_REINDEX_STMT;
    }

    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
      final SQLiteStatement statement = execute("prepare statement", new Callable() {
        @Override
        public SQLiteStatement call() throws Exception {
          return connection.prepare(sql);
        }
      });

      final long statementPtr = pointerCounter.incrementAndGet();
      statementsMap.put(statementPtr, statement);
      statementPtrsForConnection.get(connectionPtr).add(statementPtr);
      return statementPtr;
    }
  }

  void close(final long connectionPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
        execute("close connection", new Callable() {
        @Override
        public Void call() throws Exception {
          connection.dispose();
          return null;
        }
      });
      connectionsMap.remove(connectionPtr);
      statementPtrsForConnection.remove(connectionPtr);
    }
  }

  void reset() {
    ExecutorService oldDbExecutor;
    Collection openConnections;

    synchronized (lock) {
      oldDbExecutor = dbExecutor;
      openConnections = new ArrayList<>(connectionsMap.values());

      dbExecutor = Executors.newSingleThreadExecutor();
      connectionsMap.clear();
      statementsMap.clear();
      statementPtrsForConnection.clear();
    }

    shutdownDbExecutor(oldDbExecutor, openConnections);
  }

  private static void shutdownDbExecutor(ExecutorService executorService, Collection connections) {
    for (final SQLiteConnection connection : connections) {
      getFuture("close connection on reset", executorService.submit(new Callable() {
        @Override
        public Void call() throws Exception {
          connection.dispose();
          return null;
        }
      }));
    }

    executorService.shutdown();
    try {
      executorService.awaitTermination(30, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  void finalizeStmt(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return;
    }

    synchronized (lock) {
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
      statementsMap.remove(statementPtr);

        execute("finalize statement", new Callable() {
        @Override
        public Void call() throws Exception {
          statement.dispose();
          return null;
        }
      });
    }
  }

  void cancel(final long connectionPtr) {
    synchronized (lock) {
      getConnection(connectionPtr); // check connection

      for (Long statementPtr : statementPtrsForConnection.get(connectionPtr)) {
        final SQLiteStatement statement = statementsMap.get(statementPtr);
        if (statement != null) {
            execute("cancel", new Callable() {
            @Override
            public Void call() throws Exception {
              statement.cancel();
              return null;
            }
          });
        }
      }
    }
  }

  int getParameterCount(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return 0;
    }

    return executeStatementOperation(connectionPtr, statementPtr, "get parameters count in prepared statement", new StatementOperation() {
      @Override
      public Integer call(final SQLiteStatement statement) throws Exception {
        return statement.getBindParameterCount();
      }
    });
  }

  boolean isReadOnly(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return true;
    }

    return executeStatementOperation(connectionPtr, statementPtr, "call isReadOnly", new StatementOperation() {
      @Override
      public Boolean call(final SQLiteStatement statement) throws Exception {
        return statement.isReadOnly();
      }
    });
  }

  long executeForLong(final long connectionPtr, final long statementPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "execute for long", new StatementOperation() {
      @Override
      public Long call(final SQLiteStatement statement) throws Exception {
        if (!statement.step()) {
          throw new SQLiteException(SQLiteConstants.SQLITE_DONE, "No rows returned from query");
        }
        return statement.columnLong(0);
      }
    });
  }

  void executeStatement(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return;
    }

    executeStatementOperation(connectionPtr, statementPtr, "execute", new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.stepThrough();
        return null;
      }
    });
  }

  String executeForString(final long connectionPtr, final long statementPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "execute for string", new StatementOperation() {
      @Override
      public String call(final SQLiteStatement statement) throws Exception {
        if (!statement.step()) {
          throw new SQLiteException(SQLiteConstants.SQLITE_DONE, "No rows returned from query");
        }
        return statement.columnString(0);
      }
    });
  }

  int getColumnCount(final long connectionPtr, final long statementPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "get columns count", new StatementOperation() {
      @Override
      public Integer call(final SQLiteStatement statement) throws Exception {
        return statement.columnCount();
      }
    });
  }

  String getColumnName(final long connectionPtr, final long statementPtr, final int index) {
    return executeStatementOperation(connectionPtr, statementPtr, "get column name at index " + index, new StatementOperation() {
      @Override
      public String call(final SQLiteStatement statement) throws Exception {
        return statement.getColumnName(index);
      }
    });
  }

  void bindNull(final long connectionPtr, final long statementPtr, final int index) {
    executeStatementOperation(connectionPtr, statementPtr, "bind null at index " + index, new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bindNull(index);
        return null;
      }
    });
  }

  void bindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind long at index " + index + " with value " + value, new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  void bindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind double at index " + index + " with value " + value, new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  void bindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind string at index " + index, new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  void bindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind blob at index " + index, new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  int executeForChangedRowCount(final long connectionPtr, final long statementPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);

      return execute("execute for changed row count", new Callable() {
        @Override
        public Integer call() throws Exception {
          statement.stepThrough();
          return connection.getChanges();
        }
      });
    }
  }

  long executeForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);

      return execute("execute for last inserted row ID", new Callable() {
        @Override
        public Long call() throws Exception {
          statement.stepThrough();
          return connection.getLastInsertId();
        }
      });
    }
  }

  long executeForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "execute for cursor window", new StatementOperation() {
      @Override
      public Integer call(final SQLiteStatement statement) throws Exception {
        return ShadowCursorWindow.setData(windowPtr, statement);
      }
    });
  }

  void resetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
    executeStatementOperation(connectionPtr, statementPtr, "reset statement", new StatementOperation() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.reset(true);
        return null;
      }
    });
  }

  interface StatementOperation {
    T call(final SQLiteStatement statement) throws Exception;
  }

  private  T executeStatementOperation(final long connectionPtr,
                                          final long statementPtr,
                                          final String comment,
                                          final StatementOperation statementOperation) {
    synchronized (lock) {
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
      return execute(comment, new Callable() {
        @Override
        public T call() throws Exception {
          return statementOperation.call(statement);
        }
      });
    }
  }

  /**
   * Any Callable passed in to execute must not synchronize on lock, as this will result in a deadlock
   */
  private  T execute(final String comment, final Callable work) {
    synchronized (lock) {
      return getFuture(comment, dbExecutor.submit(work));
    }
  }

  private static  T getFuture(final String comment, final Future future) {
    try {
      return Uninterruptibles.getUninterruptibly(future);
      // No need to catch cancellationexception - we never cancel these futures
    } catch (ExecutionException e) {
      Throwable t = e.getCause();
      if (t instanceof SQLiteException) {
        final RuntimeException sqlException = getSqliteException("Cannot " + comment, ((SQLiteException) t).getBaseErrorCode());
        sqlException.initCause(e);
        throw sqlException;
      } else {
        throw new RuntimeException(e);
      }
    }
  }

  private static RuntimeException getSqliteException(final String message, final int baseErrorCode) {
    // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
    switch (baseErrorCode) {
      case SQLiteConstants.SQLITE_ABORT: return new SQLiteAbortException(message);
      case SQLiteConstants.SQLITE_PERM: return new SQLiteAccessPermException(message);
      case SQLiteConstants.SQLITE_RANGE: return new SQLiteBindOrColumnIndexOutOfRangeException(message);
      case SQLiteConstants.SQLITE_TOOBIG: return new SQLiteBlobTooBigException(message);
      case SQLiteConstants.SQLITE_CANTOPEN: return new SQLiteCantOpenDatabaseException(message);
      case SQLiteConstants.SQLITE_CONSTRAINT: return new SQLiteConstraintException(message);
      case SQLiteConstants.SQLITE_NOTADB: // fall through
      case SQLiteConstants.SQLITE_CORRUPT: return new SQLiteDatabaseCorruptException(message);
      case SQLiteConstants.SQLITE_BUSY: return new SQLiteDatabaseLockedException(message);
      case SQLiteConstants.SQLITE_MISMATCH: return new SQLiteDatatypeMismatchException(message);
      case SQLiteConstants.SQLITE_IOERR: return new SQLiteDiskIOException(message);
      case SQLiteConstants.SQLITE_DONE: return new SQLiteDoneException(message);
      case SQLiteConstants.SQLITE_FULL: return new SQLiteFullException(message);
      case SQLiteConstants.SQLITE_MISUSE: return new SQLiteMisuseException(message);
      case SQLiteConstants.SQLITE_NOMEM: return new SQLiteOutOfMemoryException(message);
      case SQLiteConstants.SQLITE_READONLY: return new SQLiteReadOnlyDatabaseException(message);
      case SQLiteConstants.SQLITE_LOCKED: return new SQLiteTableLockedException(message);
      case SQLiteConstants.SQLITE_INTERRUPT: return new OperationCanceledException(message);
      default: return new android.database.sqlite.SQLiteException(message
          + ", base error code: " + baseErrorCode);
    }
  }
}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy