Convert SqlUtil to Kotlin.

This commit is contained in:
Greyson Parrelli 2022-03-29 16:36:17 -04:00 committed by Cody Henthorne
parent 0e4187b062
commit 390b7ff834
3 changed files with 250 additions and 311 deletions

View file

@ -1,293 +0,0 @@
package org.signal.core.util;
import android.content.ContentValues;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public final class SqlUtil {
/** The maximum number of arguments (i.e. question marks) allowed in a SQL statement. */
private static final int MAX_QUERY_ARGS = 999;
private SqlUtil() {}
public static boolean tableExists(@NonNull SupportSQLiteDatabase db, @NonNull String table) {
try (Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type=? AND name=?", new String[] { "table", table })) {
return cursor != null && cursor.moveToNext();
}
}
public static @NonNull List<String> getAllTables(@NonNull SupportSQLiteDatabase db) {
List<String> tables = new LinkedList<>();
try (Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type=?", new String[] { "table" })) {
while (cursor.moveToNext()) {
tables.add(cursor.getString(0));
}
}
return tables;
}
/**
* Splits a multi-statement SQL block into independent statements. It is assumed that there is
* only one statement per line, and that each statement is terminated by a semi-colon.
*/
public static @NonNull List<String> splitStatements(@NonNull String sql) {
return Arrays.stream(sql.split(";\n"))
.map(String::trim)
.collect(Collectors.toList());
}
public static boolean isEmpty(@NonNull SupportSQLiteDatabase db, @NonNull String table) {
try (Cursor cursor = db.query("SELECT COUNT(*) FROM " + table, null)) {
if (cursor.moveToFirst()) {
return cursor.getInt(0) == 0;
} else {
return true;
}
}
}
public static boolean columnExists(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String column) {
try (Cursor cursor = db.query("PRAGMA table_info(" + table + ")", null)) {
int nameColumnIndex = cursor.getColumnIndexOrThrow("name");
while (cursor.moveToNext()) {
String name = cursor.getString(nameColumnIndex);
if (name.equals(column)) {
return true;
}
}
}
return false;
}
public static String[] buildArgs(Object... objects) {
String[] args = new String[objects.length];
for (int i = 0; i < objects.length; i++) {
if (objects[i] == null) {
throw new NullPointerException("Cannot have null arg!");
} else if (objects[i] instanceof DatabaseId) {
args[i] = ((DatabaseId) objects[i]).serialize();
} else {
args[i] = objects[i].toString();
}
}
return args;
}
public static String[] buildArgs(long argument) {
return new String[] { Long.toString(argument) };
}
/**
* Returns an updated query and args pairing that will only update rows that would *actually*
* change. In other words, if {@link SupportSQLiteDatabase#update(String, int, ContentValues, String, Object[])}
* returns > 0, then you know something *actually* changed.
*/
public static @NonNull Query buildTrueUpdateQuery(@NonNull String selection,
@NonNull String[] args,
@NonNull ContentValues contentValues)
{
StringBuilder qualifier = new StringBuilder();
Set<Map.Entry<String, Object>> valueSet = contentValues.valueSet();
List<String> fullArgs = new ArrayList<>(args.length + valueSet.size());
fullArgs.addAll(Arrays.asList(args));
int i = 0;
for (Map.Entry<String, Object> entry : valueSet) {
if (entry.getValue() != null) {
if (entry.getValue() instanceof byte[]) {
byte[] data = (byte[]) entry.getValue();
qualifier.append("hex(").append(entry.getKey()).append(") != ? OR ").append(entry.getKey()).append(" IS NULL");
fullArgs.add(Hex.toStringCondensed(data).toUpperCase(Locale.US));
} else {
qualifier.append(entry.getKey()).append(" != ? OR ").append(entry.getKey()).append(" IS NULL");
fullArgs.add(String.valueOf(entry.getValue()));
}
} else {
qualifier.append(entry.getKey()).append(" NOT NULL");
}
if (i != valueSet.size() - 1) {
qualifier.append(" OR ");
}
i++;
}
return new Query("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
}
public static @NonNull Query buildCollectionQuery(@NonNull String column, @NonNull Collection<? extends Object> values) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Must have values!");
}
StringBuilder query = new StringBuilder();
Object[] args = new Object[values.size()];
int i = 0;
for (Object value : values) {
query.append("?");
args[i] = value;
if (i != values.size() - 1) {
query.append(", ");
}
i++;
}
return new Query(column + " IN (" + query.toString() + ")", buildArgs(args));
}
public static @NonNull List<Query> buildCustomCollectionQuery(@NonNull String query, @NonNull List<String[]> argList) {
return buildCustomCollectionQuery(query, argList, MAX_QUERY_ARGS);
}
@VisibleForTesting
static @NonNull List<Query> buildCustomCollectionQuery(@NonNull String query, @NonNull List<String[]> argList, int maxQueryArgs) {
int batchSize = maxQueryArgs / argList.get(0).length;
return ListUtil.chunk(argList, batchSize)
.stream()
.map(argBatch -> buildSingleCustomCollectionQuery(query, argBatch))
.collect(Collectors.toList());
}
private static @NonNull Query buildSingleCustomCollectionQuery(@NonNull String query, @NonNull List<String[]> argList) {
StringBuilder outputQuery = new StringBuilder();
String[] outputArgs = new String[argList.get(0).length * argList.size()];
int argPosition = 0;
for (int i = 0, len = argList.size(); i < len; i++) {
outputQuery.append("(").append(query).append(")");
if (i < len - 1) {
outputQuery.append(" OR ");
}
String[] args = argList.get(i);
for (String arg : args) {
outputArgs[argPosition] = arg;
argPosition++;
}
}
return new Query(outputQuery.toString(), outputArgs);
}
public static @NonNull Query buildQuery(@NonNull String where, @NonNull Object... args) {
return new SqlUtil.Query(where, SqlUtil.buildArgs(args));
}
public static String[] appendArg(@NonNull String[] args, String addition) {
String[] output = new String[args.length + 1];
System.arraycopy(args, 0, output, 0, args.length);
output[output.length - 1] = addition;
return output;
}
public static List<Query> buildBulkInsert(@NonNull String tableName, @NonNull String[] columns, List<ContentValues> contentValues) {
return buildBulkInsert(tableName, columns, contentValues, MAX_QUERY_ARGS);
}
@VisibleForTesting
static List<Query> buildBulkInsert(@NonNull String tableName, @NonNull String[] columns, List<ContentValues> contentValues, int maxQueryArgs) {
int batchSize = maxQueryArgs / columns.length;
return ListUtil.chunk(contentValues, batchSize)
.stream()
.map(batch -> buildSingleBulkInsert(tableName, columns, batch))
.collect(Collectors.toList());
}
private static Query buildSingleBulkInsert(@NonNull String tableName, @NonNull String[] columns, List<ContentValues> contentValues) {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO ").append(tableName).append(" (");
for (int i = 0; i < columns.length; i++) {
builder.append(columns[i]);
if (i < columns.length - 1) {
builder.append(", ");
}
}
builder.append(") VALUES ");
StringBuilder placeholder = new StringBuilder();
placeholder.append("(");
for (int i = 0; i < columns.length; i++) {
placeholder.append("?");
if (i < columns.length - 1) {
placeholder.append(", ");
}
}
placeholder.append(")");
for (int i = 0, len = contentValues.size(); i < len; i++) {
builder.append(placeholder);
if (i < len - 1) {
builder.append(", ");
}
}
String query = builder.toString();
String[] args = new String[columns.length * contentValues.size()];
int i = 0;
for (ContentValues values : contentValues) {
for (String column : columns) {
Object value = values.get(column);
args[i] = value != null ? values.get(column).toString() : "null";
i++;
}
}
return new Query(query, args);
}
public static class Query {
private final String where;
private final String[] whereArgs;
private Query(@NonNull String where, @NonNull String[] whereArgs) {
this.where = where;
this.whereArgs = whereArgs;
}
public String getWhere() {
return where;
}
public String[] getWhereArgs() {
return whereArgs;
}
}
}

View file

@ -0,0 +1,250 @@
package org.signal.core.util
import androidx.sqlite.db.SupportSQLiteDatabase
import android.content.ContentValues
import androidx.annotation.VisibleForTesting
import java.lang.NullPointerException
import java.lang.StringBuilder
import java.util.ArrayList
import java.util.LinkedList
import java.util.Locale
import java.util.stream.Collectors
object SqlUtil {
/** The maximum number of arguments (i.e. question marks) allowed in a SQL statement. */
private const val MAX_QUERY_ARGS = 999
@JvmStatic
fun tableExists(db: SupportSQLiteDatabase, table: String): Boolean {
db.query("SELECT name FROM sqlite_master WHERE type=? AND name=?", arrayOf("table", table)).use { cursor ->
return cursor != null && cursor.moveToNext()
}
}
@JvmStatic
fun getAllTables(db: SupportSQLiteDatabase): List<String> {
val tables: MutableList<String> = LinkedList()
db.query("SELECT name FROM sqlite_master WHERE type=?", arrayOf("table")).use { cursor ->
while (cursor.moveToNext()) {
tables.add(cursor.getString(0))
}
}
return tables
}
@JvmStatic
fun isEmpty(db: SupportSQLiteDatabase, table: String): Boolean {
db.query("SELECT COUNT(*) FROM $table", null).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getInt(0) == 0
} else {
true
}
}
}
@JvmStatic
fun columnExists(db: SupportSQLiteDatabase, table: String, column: String): Boolean {
db.query("PRAGMA table_info($table)", null).use { cursor ->
val nameColumnIndex = cursor.getColumnIndexOrThrow("name")
while (cursor.moveToNext()) {
val name = cursor.getString(nameColumnIndex)
if (name == column) {
return true
}
}
}
return false
}
@JvmStatic
fun buildArgs(vararg objects: Any?): Array<String> {
return objects.map {
when (it) {
null -> throw NullPointerException("Cannot have null arg!")
is DatabaseId -> (it as DatabaseId?)!!.serialize()
else -> it.toString()
}
}.toTypedArray()
}
@JvmStatic
fun buildArgs(argument: Long): Array<String> {
return arrayOf(argument.toString())
}
/**
* Returns an updated query and args pairing that will only update rows that would *actually*
* change. In other words, if [SupportSQLiteDatabase.update]
* returns > 0, then you know something *actually* changed.
*/
@JvmStatic
fun buildTrueUpdateQuery(
selection: String,
args: Array<String>,
contentValues: ContentValues
): Query {
val qualifier = StringBuilder()
val valueSet = contentValues.valueSet()
val fullArgs: MutableList<String> = ArrayList(args.size + valueSet.size)
fullArgs.addAll(args)
var i = 0
for ((key, value) in valueSet) {
if (value != null) {
if (value is ByteArray) {
qualifier.append("hex(").append(key).append(") != ? OR ").append(key).append(" IS NULL")
fullArgs.add(Hex.toStringCondensed(value).toUpperCase(Locale.US))
} else {
qualifier.append(key).append(" != ? OR ").append(key).append(" IS NULL")
fullArgs.add(value.toString())
}
} else {
qualifier.append(key).append(" NOT NULL")
}
if (i != valueSet.size - 1) {
qualifier.append(" OR ")
}
i++
}
return Query("($selection) AND ($qualifier)", fullArgs.toTypedArray())
}
@JvmStatic
fun buildCollectionQuery(column: String, values: Collection<Any?>): Query {
require(!values.isEmpty()) { "Must have values!" }
val query = StringBuilder()
val args = arrayOfNulls<Any>(values.size)
var i = 0
for (value in values) {
query.append("?")
args[i] = value
if (i != values.size - 1) {
query.append(", ")
}
i++
}
return Query("$column IN ($query)", buildArgs(*args))
}
@JvmStatic
fun buildCustomCollectionQuery(query: String, argList: List<Array<String>>): List<Query> {
return buildCustomCollectionQuery(query, argList, MAX_QUERY_ARGS)
}
@JvmStatic
@VisibleForTesting
fun buildCustomCollectionQuery(query: String, argList: List<Array<String>>, maxQueryArgs: Int): List<Query> {
val batchSize: Int = maxQueryArgs / argList[0].size
return ListUtil.chunk(argList, batchSize)
.stream()
.map { argBatch -> buildSingleCustomCollectionQuery(query, argBatch) }
.collect(Collectors.toList())
}
private fun buildSingleCustomCollectionQuery(query: String, argList: List<Array<String>>): Query {
val outputQuery = StringBuilder()
val outputArgs: MutableList<String> = mutableListOf()
var i = 0
val len = argList.size
while (i < len) {
outputQuery.append("(").append(query).append(")")
if (i < len - 1) {
outputQuery.append(" OR ")
}
val args = argList[i]
for (arg in args) {
outputArgs += arg
}
i++
}
return Query(outputQuery.toString(), outputArgs.toTypedArray())
}
@JvmStatic
fun buildQuery(where: String, vararg args: Any): Query {
return Query(where, buildArgs(*args))
}
@JvmStatic
fun appendArg(args: Array<String>, addition: String): Array<String> {
return args.toMutableList().apply {
add(addition)
}.toTypedArray()
}
@JvmStatic
fun buildBulkInsert(tableName: String, columns: Array<String>, contentValues: List<ContentValues>): List<Query> {
return buildBulkInsert(tableName, columns, contentValues, MAX_QUERY_ARGS)
}
@JvmStatic
@VisibleForTesting
fun buildBulkInsert(tableName: String, columns: Array<String>, contentValues: List<ContentValues>, maxQueryArgs: Int): List<Query> {
val batchSize = maxQueryArgs / columns.size
return contentValues
.chunked(batchSize)
.map { batch: List<ContentValues> -> buildSingleBulkInsert(tableName, columns, batch) }
.toList()
}
private fun buildSingleBulkInsert(tableName: String, columns: Array<String>, contentValues: List<ContentValues>): Query {
val builder = StringBuilder()
builder.append("INSERT INTO ").append(tableName).append(" (")
for (i in columns.indices) {
builder.append(columns[i])
if (i < columns.size - 1) {
builder.append(", ")
}
}
builder.append(") VALUES ")
val placeholder = StringBuilder()
placeholder.append("(")
for (i in columns.indices) {
placeholder.append("?")
if (i < columns.size - 1) {
placeholder.append(", ")
}
}
placeholder.append(")")
var i = 0
val len = contentValues.size
while (i < len) {
builder.append(placeholder)
if (i < len - 1) {
builder.append(", ")
}
i++
}
val query = builder.toString()
val args: MutableList<String> = mutableListOf()
for (values in contentValues) {
for (column in columns) {
val value = values[column]
args += if (value != null) values[column].toString() else "null"
}
}
return Query(query, args.toTypedArray())
}
class Query(val where: String, val whereArgs: Array<String>)
}

View file

@ -186,24 +186,6 @@ public final class SqlUtilTest {
assertArrayEquals(new String[] { "5", "6" }, queries.get(1).getWhereArgs()); assertArrayEquals(new String[] { "5", "6" }, queries.get(1).getWhereArgs());
} }
@Test
public void splitStatements_singleStatement() {
List<String> result = SqlUtil.splitStatements("SELECT * FROM foo;\n");
assertEquals(Arrays.asList("SELECT * FROM foo"), result);
}
@Test
public void splitStatements_twoStatements() {
List<String> result = SqlUtil.splitStatements("SELECT * FROM foo;\nSELECT * FROM bar;\n");
assertEquals(Arrays.asList("SELECT * FROM foo", "SELECT * FROM bar"), result);
}
@Test
public void splitStatements_twoStatementsSeparatedByNewLines() {
List<String> result = SqlUtil.splitStatements("SELECT * FROM foo;\n\nSELECT * FROM bar;\n");
assertEquals(Arrays.asList("SELECT * FROM foo", "SELECT * FROM bar"), result);
}
@Test @Test
public void buildBulkInsert_single_singleBatch() { public void buildBulkInsert_single_singleBatch() {
List<ContentValues> contentValues = new ArrayList<>(); List<ContentValues> contentValues = new ArrayList<>();