Convert SqlUtil to Kotlin.
This commit is contained in:
parent
0e4187b062
commit
390b7ff834
3 changed files with 250 additions and 311 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
250
core-util/src/main/java/org/signal/core/util/SqlUtil.kt
Normal file
250
core-util/src/main/java/org/signal/core/util/SqlUtil.kt
Normal 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>)
|
||||
}
|
|
@ -186,24 +186,6 @@ public final class SqlUtilTest {
|
|||
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
|
||||
public void buildBulkInsert_single_singleBatch() {
|
||||
List<ContentValues> contentValues = new ArrayList<>();
|
||||
|
|
Loading…
Add table
Reference in a new issue