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

org.robolectric.shadows.ShadowLegacySQLiteConnection 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 android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.S_V2;
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.ThreadFactory;
import java.util.concurrent.TimeUnit;
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;
import org.robolectric.util.PerfStatsCollector;

/** Shadow for {@link android.database.sqlite.SQLiteConnection} that is backed by sqlite4java. */
@Implements(value = android.database.sqlite.SQLiteConnection.class, isInAndroidSdk = false)
public class ShadowLegacySQLiteConnection extends 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;

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

  @Implementation(minSdk = O_MR1)
  protected 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)
  protected static int nativePrepareStatement(int connectionPtr, String sql) {
    return (int) nativePrepareStatement((long) connectionPtr, sql);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected 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();
  }

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

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

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

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

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

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

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

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

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

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

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

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

  @Implementation(minSdk = 33)
  protected static void nativeExecute(
      final long connectionPtr, final long statementPtr, boolean isPragmaStmt) {
    CONNECTIONS.executeStatement(connectionPtr, statementPtr);
  }

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

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

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

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

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

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

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

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

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

  @Implementation(minSdk = LOLLIPOP)
  protected 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)
  protected static void nativeBindDouble(
      int connectionPtr, int statementPtr, int index, double value) {
    nativeBindDouble((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected 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)
  protected static void nativeBindString(
      int connectionPtr, int statementPtr, int index, String value) {
    nativeBindString((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected 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)
  protected static void nativeBindBlob(
      int connectionPtr, int statementPtr, int index, byte[] value) {
    nativeBindBlob((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected 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)
  protected static void nativeRegisterLocalizedCollators(int connectionPtr, String locale) {
    nativeRegisterLocalizedCollators((long) connectionPtr, locale);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected 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)
  protected static int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr) {
    return nativeExecuteForChangedRowCount((long) connectionPtr, (long) statementPtr);
  }

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

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

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

  @Implementation(maxSdk = KITKAT_WATCH)
  protected 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)
  protected 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)
  protected static void nativeResetStatementAndClearBindings(int connectionPtr, int statementPtr) {
    nativeResetStatementAndClearBindings((long) connectionPtr, (long) statementPtr);
  }

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

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

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

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

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

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

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

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

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

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

  @Implementation(minSdk = LOLLIPOP)
  protected 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(threadFactory());

    static ThreadFactory threadFactory() {
      ThreadFactory delegate = Executors.defaultThreadFactory();
      return r -> {
        Thread worker = delegate.newThread(r);
        worker.setName(ShadowLegacySQLiteConnection.class.getSimpleName() + " worker");
        return worker;
      };
    }

  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(threadFactory());
      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 {
                if (statement.step()) {
                  throw new android.database.sqlite.SQLiteException(
                      "Queries can be performed using SQLiteDatabase query or rawQuery methods"
                          + " only.");
                }
                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.getChanges() > 0 ? connection.getLastInsertId() : -1L;
              }
            });
    }
  }

  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 ShadowLegacyCursorWindow.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 PerfStatsCollector.getInstance()
            .measure("sqlite", () -> 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 if (t instanceof android.database.sqlite.SQLiteException) {
          throw (android.database.sqlite.SQLiteException) t;
      } 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