From 390b7ff834f51fe3ee7c4afa3d0d19387ed00c14 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 29 Mar 2022 16:36:17 -0400 Subject: [PATCH] Convert SqlUtil to Kotlin. --- .../java/org/signal/core/util/SqlUtil.java | 293 ------------------ .../main/java/org/signal/core/util/SqlUtil.kt | 250 +++++++++++++++ .../org/signal/core/util/SqlUtilTest.java | 18 -- 3 files changed, 250 insertions(+), 311 deletions(-) delete mode 100644 core-util/src/main/java/org/signal/core/util/SqlUtil.java create mode 100644 core-util/src/main/java/org/signal/core/util/SqlUtil.kt diff --git a/core-util/src/main/java/org/signal/core/util/SqlUtil.java b/core-util/src/main/java/org/signal/core/util/SqlUtil.java deleted file mode 100644 index 8d13333f24..0000000000 --- a/core-util/src/main/java/org/signal/core/util/SqlUtil.java +++ /dev/null @@ -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 getAllTables(@NonNull SupportSQLiteDatabase db) { - List 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 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> valueSet = contentValues.valueSet(); - List fullArgs = new ArrayList<>(args.length + valueSet.size()); - - fullArgs.addAll(Arrays.asList(args)); - - int i = 0; - - for (Map.Entry 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 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 buildCustomCollectionQuery(@NonNull String query, @NonNull List argList) { - return buildCustomCollectionQuery(query, argList, MAX_QUERY_ARGS); - } - - @VisibleForTesting - static @NonNull List buildCustomCollectionQuery(@NonNull String query, @NonNull List 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 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 buildBulkInsert(@NonNull String tableName, @NonNull String[] columns, List contentValues) { - return buildBulkInsert(tableName, columns, contentValues, MAX_QUERY_ARGS); - } - - @VisibleForTesting - static List buildBulkInsert(@NonNull String tableName, @NonNull String[] columns, List 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) { - 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; - } - } -} diff --git a/core-util/src/main/java/org/signal/core/util/SqlUtil.kt b/core-util/src/main/java/org/signal/core/util/SqlUtil.kt new file mode 100644 index 0000000000..f457cfc9d6 --- /dev/null +++ b/core-util/src/main/java/org/signal/core/util/SqlUtil.kt @@ -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 { + val tables: MutableList = 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 { + 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 { + 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, + contentValues: ContentValues + ): Query { + val qualifier = StringBuilder() + val valueSet = contentValues.valueSet() + + val fullArgs: MutableList = 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): Query { + require(!values.isEmpty()) { "Must have values!" } + + val query = StringBuilder() + val args = arrayOfNulls(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>): List { + return buildCustomCollectionQuery(query, argList, MAX_QUERY_ARGS) + } + + @JvmStatic + @VisibleForTesting + fun buildCustomCollectionQuery(query: String, argList: List>, maxQueryArgs: Int): List { + 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>): Query { + val outputQuery = StringBuilder() + val outputArgs: MutableList = 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, addition: String): Array { + return args.toMutableList().apply { + add(addition) + }.toTypedArray() + } + + @JvmStatic + fun buildBulkInsert(tableName: String, columns: Array, contentValues: List): List { + return buildBulkInsert(tableName, columns, contentValues, MAX_QUERY_ARGS) + } + + @JvmStatic + @VisibleForTesting + fun buildBulkInsert(tableName: String, columns: Array, contentValues: List, maxQueryArgs: Int): List { + val batchSize = maxQueryArgs / columns.size + + return contentValues + .chunked(batchSize) + .map { batch: List -> buildSingleBulkInsert(tableName, columns, batch) } + .toList() + } + + private fun buildSingleBulkInsert(tableName: String, columns: Array, contentValues: List): 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 = 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) +} \ No newline at end of file diff --git a/core-util/src/test/java/org/signal/core/util/SqlUtilTest.java b/core-util/src/test/java/org/signal/core/util/SqlUtilTest.java index 0662cad03d..a604380d63 100644 --- a/core-util/src/test/java/org/signal/core/util/SqlUtilTest.java +++ b/core-util/src/test/java/org/signal/core/util/SqlUtilTest.java @@ -186,24 +186,6 @@ public final class SqlUtilTest { assertArrayEquals(new String[] { "5", "6" }, queries.get(1).getWhereArgs()); } - @Test - public void splitStatements_singleStatement() { - List result = SqlUtil.splitStatements("SELECT * FROM foo;\n"); - assertEquals(Arrays.asList("SELECT * FROM foo"), result); - } - - @Test - public void splitStatements_twoStatements() { - List 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 result = SqlUtil.splitStatements("SELECT * FROM foo;\n\nSELECT * FROM bar;\n"); - assertEquals(Arrays.asList("SELECT * FROM foo", "SELECT * FROM bar"), result); - } - @Test public void buildBulkInsert_single_singleBatch() { List contentValues = new ArrayList<>();