Add extension functions to improve writability of database queries.

This commit is contained in:
Greyson Parrelli 2022-04-01 10:04:05 -04:00 committed by Cody Henthorne
parent 3e42c044b8
commit 98b9cc23e4
2 changed files with 317 additions and 2 deletions

View file

@ -3,8 +3,12 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
import net.zetetic.database.SQLException; import net.zetetic.database.SQLException;
import net.zetetic.database.sqlcipher.SQLiteStatement; import net.zetetic.database.sqlcipher.SQLiteStatement;
@ -12,9 +16,11 @@ import net.zetetic.database.sqlcipher.SQLiteTransactionListener;
import org.signal.core.util.tracing.Tracer; import org.signal.core.util.tracing.Tracer;
import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -25,7 +31,7 @@ import java.util.Set;
* making a subclass, so instead we just match the interface. Callers should just need to change * making a subclass, so instead we just match the interface. Callers should just need to change
* their import statements. * their import statements.
*/ */
public class SQLiteDatabase { public class SQLiteDatabase implements SupportSQLiteDatabase {
public static final int CONFLICT_ROLLBACK = 1; public static final int CONFLICT_ROLLBACK = 1;
public static final int CONFLICT_ABORT = 2; public static final int CONFLICT_ABORT = 2;
@ -183,6 +189,77 @@ public class SQLiteDatabase {
} }
} }
// =======================================================
// Overrides
// =======================================================
@Override
public void beginTransactionWithListener(android.database.sqlite.SQLiteTransactionListener transactionListener) {
beginTransactionWithListener(new ConvertedTransactionListener(transactionListener));
}
@Override
public void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener transactionListener) {
beginTransactionWithListenerNonExclusive(new ConvertedTransactionListener(transactionListener));
}
@Override
public Cursor query(String query) {
return rawQuery(query, null);
}
@Override
public Cursor query(String query, Object[] bindArgs) {
return rawQuery(query, bindArgs);
}
@Override
public Cursor query(SupportSQLiteQuery query) {
DatabaseMonitor.onSql(query.getSql(), null);
return wrapped.query(query);
}
@Override
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
DatabaseMonitor.onSql(query.getSql(), null);
return wrapped.query(query, cancellationSignal);
}
@Override
public long insert(String table, int conflictAlgorithm, ContentValues values) throws android.database.SQLException {
return insertWithOnConflict(table, null, values, conflictAlgorithm);
}
@Override
public int delete(String table, String whereClause, Object[] whereArgs) {
return delete(table, whereClause, (String[]) whereArgs);
}
@Override
public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause, Object[] whereArgs) {
return updateWithOnConflict(table, values, whereClause, (String[]) whereArgs, conflictAlgorithm);
}
@Override
public void setMaxSqlCacheSize(int cacheSize) {
wrapped.setMaxSqlCacheSize(cacheSize);
}
@Override
public List<Pair<String, String>> getAttachedDbs() {
return wrapped.getAttachedDbs();
}
@Override
public boolean isDatabaseIntegrityOk() {
return wrapped.isDatabaseIntegrityOk();
}
@Override
public void close() throws IOException {
wrapped.close();
}
// ======================================================= // =======================================================
// Traced // Traced
@ -297,6 +374,7 @@ public class SQLiteDatabase {
} }
public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, int conflictAlgorithm) { public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, int conflictAlgorithm) {
DatabaseMonitor.onUpdate(table, values, whereClause, whereArgs);
return traceSql("updateWithOnConflict()", table, whereClause, true, () -> wrapped.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm)); return traceSql("updateWithOnConflict()", table, whereClause, true, () -> wrapped.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm));
} }
@ -415,4 +493,28 @@ public class SQLiteDatabase {
public void setLocale(Locale locale) { public void setLocale(Locale locale) {
wrapped.setLocale(locale); wrapped.setLocale(locale);
} }
private static class ConvertedTransactionListener implements SQLiteTransactionListener {
private final android.database.sqlite.SQLiteTransactionListener listener;
ConvertedTransactionListener(android.database.sqlite.SQLiteTransactionListener listener) {
this.listener = listener;
}
@Override
public void onBegin() {
listener.onBegin();
}
@Override
public void onCommit() {
listener.onCommit();
}
@Override
public void onRollback() {
listener.onRollback();
}
}
} }

View file

@ -1,6 +1,11 @@
package org.signal.core.util package org.signal.core.util
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import androidx.core.content.contentValuesOf
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQueryBuilder
fun SupportSQLiteDatabase.getTableRowCount(table: String): Int { fun SupportSQLiteDatabase.getTableRowCount(table: String): Int {
return this.query("SELECT COUNT(*) FROM $table").use { return this.query("SELECT COUNT(*) FROM $table").use {
@ -10,4 +15,212 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int {
0 0
} }
} }
} }
/**
* Begins a SELECT statement with a helpful builder pattern.
*/
fun SupportSQLiteDatabase.select(vararg columns: String): SelectBuilderPart1 {
return SelectBuilderPart1(this, arrayOf(*columns))
}
/**
* Begins an UPDATE statement with a helpful builder pattern.
*/
fun SupportSQLiteDatabase.update(tableName: String): UpdateBuilderPart1 {
return UpdateBuilderPart1(this, tableName)
}
/**
* Begins a DELETE statement with a helpful builder pattern.
*/
fun SupportSQLiteDatabase.delete(tableName: String): DeleteBuilderPart1 {
return DeleteBuilderPart1(this, tableName)
}
class SelectBuilderPart1(
private val db: SupportSQLiteDatabase,
private val columns: Array<String>
) {
fun from(tableName: String): SelectBuilderPart2 {
return SelectBuilderPart2(db, columns, tableName)
}
}
class SelectBuilderPart2(
private val db: SupportSQLiteDatabase,
private val columns: Array<String>,
private val tableName: String
) {
fun where(where: String, vararg whereArgs: Any): SelectBuilderPart3 {
return SelectBuilderPart3(db, columns, tableName, where, SqlUtil.buildArgs(whereArgs))
}
fun run(): Cursor {
return db.query(
SupportSQLiteQueryBuilder
.builder(tableName)
.columns(columns)
.create()
)
}
}
class SelectBuilderPart3(
private val db: SupportSQLiteDatabase,
private val columns: Array<String>,
private val tableName: String,
private val where: String,
private val whereArgs: Array<String>
) {
fun orderBy(orderBy: String): SelectBuilderPart4a {
return SelectBuilderPart4a(db, columns, tableName, where, whereArgs, orderBy)
}
fun limit(limit: Int): SelectBuilderPart4b {
return SelectBuilderPart4b(db, columns, tableName, where, whereArgs, limit.toString())
}
fun run(): Cursor {
return db.query(
SupportSQLiteQueryBuilder
.builder(tableName)
.columns(columns)
.selection(where, whereArgs)
.create()
)
}
}
class SelectBuilderPart4a(
private val db: SupportSQLiteDatabase,
private val columns: Array<String>,
private val tableName: String,
private val where: String,
private val whereArgs: Array<String>,
private val orderBy: String
) {
fun limit(limit: Int): SelectBuilderPart5 {
return SelectBuilderPart5(db, columns, tableName, where, whereArgs, orderBy, limit.toString())
}
fun run(): Cursor {
return db.query(
SupportSQLiteQueryBuilder
.builder(tableName)
.columns(columns)
.selection(where, whereArgs)
.orderBy(orderBy)
.create()
)
}
}
class SelectBuilderPart4b(
private val db: SupportSQLiteDatabase,
private val columns: Array<String>,
private val tableName: String,
private val where: String,
private val whereArgs: Array<String>,
private val limit: String
) {
fun orderBy(orderBy: String): SelectBuilderPart5 {
return SelectBuilderPart5(db, columns, tableName, where, whereArgs, orderBy, limit)
}
fun run(): Cursor {
return db.query(
SupportSQLiteQueryBuilder
.builder(tableName)
.columns(columns)
.selection(where, whereArgs)
.limit(limit)
.create()
)
}
}
class SelectBuilderPart5(
private val db: SupportSQLiteDatabase,
private val columns: Array<String>,
private val tableName: String,
private val where: String,
private val whereArgs: Array<String>,
private val orderBy: String,
private val limit: String
) {
fun run(): Cursor {
return db.query(
SupportSQLiteQueryBuilder
.builder(tableName)
.columns(columns)
.selection(where, whereArgs)
.orderBy(orderBy)
.limit(limit)
.create()
)
}
}
class UpdateBuilderPart1(
private val db: SupportSQLiteDatabase,
private val tableName: String
) {
fun values(values: ContentValues): UpdateBuilderPart2 {
return UpdateBuilderPart2(db, tableName, values)
}
fun values(vararg values: Pair<String, Any?>): UpdateBuilderPart2 {
return UpdateBuilderPart2(db, tableName, contentValuesOf(*values))
}
}
class UpdateBuilderPart2(
private val db: SupportSQLiteDatabase,
private val tableName: String,
private val values: ContentValues
) {
fun where(where: String, vararg whereArgs: Any): UpdateBuilderPart3 {
return UpdateBuilderPart3(db, tableName, values, where, SqlUtil.buildArgs(whereArgs))
}
fun run(conflictStrategy: Int = SQLiteDatabase.CONFLICT_NONE): Int {
return db.update(tableName, conflictStrategy, values, null, null)
}
}
class UpdateBuilderPart3(
private val db: SupportSQLiteDatabase,
private val tableName: String,
private val values: ContentValues,
private val where: String,
private val whereArgs: Array<String>
) {
fun run(conflictStrategy: Int = SQLiteDatabase.CONFLICT_NONE): Int {
return db.update(tableName, conflictStrategy, values, where, whereArgs)
}
}
class DeleteBuilderPart1(
private val db: SupportSQLiteDatabase,
private val tableName: String
) {
fun where(where: String, vararg whereArgs: Any): DeleteBuilderPart2 {
return DeleteBuilderPart2(db, tableName, where, SqlUtil.buildArgs(whereArgs))
}
fun run(): Int {
return db.delete(tableName, null, null)
}
}
class DeleteBuilderPart2(
private val db: SupportSQLiteDatabase,
private val tableName: String,
private val where: String,
private val whereArgs: Array<String>
) {
fun run(): Int {
return db.delete(tableName, where, whereArgs)
}
}