From 6494035b172007cc49e06e8616dcebc910041d43 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Tue, 10 Aug 2021 16:53:47 -0500 Subject: [PATCH] Add support for rawExecSQL, add tests for rawQuery binding, fts5, rtree, soundex, savepoint --- .../sqlcipher_cts/SQLCipherDatabaseTest.java | 172 +++++++++++++++++- .../database/sqlcipher/SQLiteDatabase.java | 22 +++ .../database/sqlcipher/SQLiteSession.java | 31 ++++ .../database/sqlcipher/SQLiteStatement.java | 21 +++ 4 files changed, 240 insertions(+), 6 deletions(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java index 9d2bf16..23dc484 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java @@ -4,13 +4,16 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; +import android.content.ContentValues; import android.database.Cursor; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabaseConfiguration; import net.zetetic.database.sqlcipher.SQLiteDatabaseCorruptException; import net.zetetic.database.sqlcipher.SQLiteException; +import net.zetetic.database.sqlcipher.SQLiteStatement; +import org.hamcrest.core.Is; import org.junit.Test; import java.io.File; @@ -98,7 +101,7 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { } @Test - public void openExistingSQLCipherDatabaseWithStringPassword(){ + public void openExistingSQLCipherDatabaseWithStringPassword() { File databasePath = null; int a = 0, b = 0; try { @@ -106,7 +109,7 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { databasePath = extractAssetToDatabaseDirectory("sqlcipher-4.x-testkey.db"); database = SQLiteDatabase.openDatabase(databasePath.getPath(), "testkey", null, SQLiteDatabase.OPEN_READWRITE, null); Cursor cursor = database.rawQuery("SELECT * FROM t1;"); - if(cursor != null && cursor.moveToFirst()){ + if (cursor != null && cursor.moveToFirst()) { a = cursor.getInt(0); b = cursor.getInt(1); cursor.close(); @@ -119,7 +122,7 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { } @Test - public void openExistingSQLCipherDatabaseWithByteArrayPassword(){ + public void openExistingSQLCipherDatabaseWithByteArrayPassword() { File databasePath = null; int a = 0, b = 0; try { @@ -127,7 +130,7 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { databasePath = extractAssetToDatabaseDirectory("sqlcipher-4.x-testkey.db"); database = SQLiteDatabase.openDatabase(databasePath.getPath(), "testkey".getBytes(StandardCharsets.UTF_8), null, SQLiteDatabase.OPEN_READWRITE, null); Cursor cursor = database.rawQuery("SELECT * FROM t1;"); - if(cursor != null && cursor.moveToFirst()){ + if (cursor != null && cursor.moveToFirst()) { a = cursor.getInt(0); b = cursor.getInt(1); cursor.close(); @@ -140,7 +143,7 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { } @Test - public void openExistingSQLitePlaintextDatabase(){ + public void openExistingSQLitePlaintextDatabase() { File databasePath = null; int a = 0, b = 0; try { @@ -148,7 +151,7 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { databasePath = extractAssetToDatabaseDirectory("sqlite-plaintext.db"); database = SQLiteDatabase.openDatabase(databasePath.getPath(), "", null, SQLiteDatabase.OPEN_READWRITE, null); Cursor cursor = database.rawQuery("SELECT * FROM t1;"); - if(cursor != null && cursor.moveToFirst()){ + if (cursor != null && cursor.moveToFirst()) { a = cursor.getInt(0); b = cursor.getInt(1); cursor.close(); @@ -237,4 +240,161 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { database = SQLiteDatabase.openOrCreateDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, "foo", null, null, null); database.changePassword("bar"); } + + @Test + public void shouldPerformRawQueryWithBoolean() { + boolean a = false, b = true; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{true, false}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", false); + if (cursor != null && cursor.moveToFirst()) { + a = cursor.getInt(0) > 0; + b = cursor.getInt(1) > 0; + } + assertThat(a, is(true)); + assertThat(b, is(false)); + } + + @Test + public void shouldPerformRawQueryWithByteArray() { + byte[] a = generateRandomBytes(64); + byte[] b = generateRandomBytes(64); + byte[] aActual = null, bActual = null; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{a, b}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", b); + if (cursor != null && cursor.moveToFirst()) { + aActual = cursor.getBlob(0); + bActual = cursor.getBlob(1); + } + assertThat(aActual, is(a)); + assertThat(bActual, is(b)); + } + + @Test + public void shouldPerformRawQueryWithDouble() { + double a = 3.14d, b = 42.0d; + double aActual = 0.0d, bActual = 0.0d; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{a, b}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", b); + if (cursor != null && cursor.moveToFirst()) { + aActual = cursor.getDouble(0); + bActual = cursor.getDouble(1); + } + assertThat(aActual, is(a)); + assertThat(bActual, is(b)); + } + + @Test + public void shouldPerformRawQueryWithFloat() { + float a = 3.14f, b = 42.0f; + float aActual = 0.0f, bActual = 0.0f; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{a, b}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", b); + if (cursor != null && cursor.moveToFirst()) { + aActual = cursor.getFloat(0); + bActual = cursor.getFloat(1); + } + assertThat(aActual, is(a)); + assertThat(bActual, is(b)); + } + + @Test + public void shouldPerformRawQueryWithLong() { + long a = 3L, b = 42L; + long aActual = 0L, bActual = 0L; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{a, b}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", b); + if (cursor != null && cursor.moveToFirst()) { + aActual = cursor.getLong(0); + bActual = cursor.getLong(1); + } + assertThat(aActual, is(a)); + assertThat(bActual, is(b)); + } + + @Test + public void shouldPerformRawQueryWithString() { + String a = "one for the money", b = "two for the show"; + String aActual = "", bActual = ""; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{a, b}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", b); + if (cursor != null && cursor.moveToFirst()) { + aActual = cursor.getString(0); + bActual = cursor.getString(1); + } + assertThat(aActual, is(a)); + assertThat(bActual, is(b)); + } + + @Test + public void shouldPerformFTS5Search() { + boolean found = false; + database.execSQL("CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"); + database.execSQL("insert into email(sender, title, body) values(?, ?, ?);", + new Object[]{"foo@bar.com", "Test Email", "This is a test email message."}); + Cursor cursor = database.rawQuery("select * from email where email match ?;", "test"); + if (cursor != null && cursor.moveToFirst()) { + found = cursor.getString(cursor.getColumnIndex("sender")).equals("foo@bar.com"); + } + assertThat(found, is(true)); + } + + @Test + public void shouldPerformRTreeTest() { + int id = 0; + String create = "CREATE VIRTUAL TABLE demo_index USING rtree(id, minX, maxX, minY, maxY);"; + String insert = "INSERT INTO demo_index VALUES(?, ?, ?, ?, ?);"; + database.execSQL(create); + database.execSQL(insert, new Object[]{1, -80.7749, -80.7747, 35.3776, 35.3778}); + Cursor cursor = database.rawQuery("SELECT * FROM demo_index WHERE maxY < ?;", 36); + if (cursor != null && cursor.moveToNext()) { + id = cursor.getInt(0); + cursor.close(); + } + assertThat(id, is(1)); + } + + @Test + public void shouldPerformSoundexTest() { + String value = ""; + Cursor cursor = database.rawQuery("SELECT soundex('sqlcipher');"); + if (cursor != null && cursor.moveToFirst()) { + value = cursor.getString(0); + cursor.close(); + } + assertThat(value, is("S421")); + } + + @Test + public void shouldInsertWithOnConflictTest(){ + database.execSQL("create table user(_id integer primary key autoincrement, email text unique not null);"); + ContentValues values = new ContentValues(); + values.put("email", "foo@bar.com"); + long id = database.insertWithOnConflict("user", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + long error = database.insertWithOnConflict("user", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + assertThat(id, is(1L)); + assertThat(error, is(-1L)); + } + + @Test + public void shouldPerformRollbackToSavepoint(){ + database.rawExecSQL("savepoint foo;"); + database.rawExecSQL("create table t1(a,b);"); + database.rawExecSQL("insert into t1(a,b) values(?,?);", "one for the money", "two for the show"); + database.rawExecSQL("savepoint bar;"); + database.rawExecSQL("insert into t1(a,b) values(?,?);", "three to get ready", "go man go"); + database.rawExecSQL("rollback transaction to bar;"); + database.rawExecSQL("commit;"); + SQLiteStatement statement = database.compileStatement("select count(*) from t1 where a = ?;"); + statement.bindString(1, "one for the money"); + long count = statement.simpleQueryForLong(); + assertThat(count, is(1L)); + } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java index 6fe1cfa..a76ec64 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java @@ -1856,6 +1856,28 @@ public final class SQLiteDatabase extends SQLiteClosable { executeSql(sql, bindArgs); } + /** + * Executes a statement that returns a count of the number of rows + * that were changed. No transaction state checking is performed. + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind. + * @return The number of rows that were changed. + */ + public int rawExecSQL(String sql, Object...bindArgs) throws SQLException { + acquireReference(); + try { + SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); + try { + return statement.executeUpdateDeleteRaw(); + } finally { + statement.close(); + } + + } finally { + releaseReference(); + } + } + private int executeSql(String sql, Object[] bindArgs) throws SQLException { acquireReference(); try { diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java index 580c2a8..94a2bda 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java @@ -762,6 +762,37 @@ public final class SQLiteSession { } } + /** + * Executes a statement that returns a count of the number of rows + * that were changed. Use for UPDATE or DELETE SQL statements. Does not + * perform additional transaction process verification. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were changed. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForChangedRowCountRaw(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForChangedRowCount(sql, bindArgs, + cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + /** * Executes a statement that returns the row id of the last row inserted * by the statement. Use for INSERT SQL statements. diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java index c88a8c8..b20388a 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java @@ -75,6 +75,27 @@ public final class SQLiteStatement extends SQLiteProgram { } } + /** + * Execute this SQL statement, if the the number of rows affected by execution of this SQL + * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements. + * No transaction state checking is performed. + * @return the number of rows affected by this SQL statement execution. + * @throws android.database.SQLException If the SQL string is invalid for + * some reason + */ + public int executeUpdateDeleteRaw() { + acquireReference(); + try { + return getSession().executeForChangedRowCountRaw( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + /** * Execute this SQL statement and return the ID of the row inserted due to this call. * The SQL statement should be an INSERT for this to be a useful call.