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