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