Fix db crash on big projects

This commit is contained in:
2026-09-10 00:26:16 +02:00
parent c70bb808c1
commit 06f31571a3
@@ -238,13 +238,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBProject> { private fun getProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBProject> {
val cursor = db.query(table_projects, columnsProjects, selection, selectionArgs, null, null, orderBy) return queryAll(db, table_projects, columnsProjects, selection, selectionArgs, orderBy, ::getProjectFromCursor)
val projects: MutableList<DBProject> = ArrayList()
while (cursor.moveToNext()) {
projects.add(getProjectFromCursor(cursor))
}
cursor.close()
return projects
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -305,11 +299,17 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
fun deleteProject(id: Long) { fun deleteProject(id: Long) {
val db = writableDatabase val db = writableDatabase
for (b in getBillsOfProject(id)) { val projectId = arrayOf(id.toString())
deleteBill(b.id) inTransaction {
db.delete(
table_billowers,
"$key_billId IN (SELECT $key_id FROM $table_bills WHERE $key_projectid = ?)",
projectId
)
db.delete(table_bills, "$key_projectid = ?", projectId)
db.delete(table_members, "$key_projectid = ?", projectId)
db.delete(table_projects, "$key_id = ?", projectId)
} }
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
SecureStorage.removePasswordSync(context, "ProjectPassword_$id") SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
} }
@@ -413,14 +413,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getMembersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBMember> { private fun getMembersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBMember> {
val db = readableDatabase return queryAll(readableDatabase, table_members, columnsMembers, selection, selectionArgs, orderBy, ::getMemberFromCursor)
val cursor = db.query(table_members, columnsMembers, selection, selectionArgs, null, null, orderBy)
val members: MutableList<DBMember> = ArrayList()
while (cursor.moveToNext()) {
members.add(getMemberFromCursor(cursor))
}
cursor.close()
return members
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -644,29 +637,20 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getBillsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBill> { private fun getBillsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBill> {
val db = readableDatabase val db = readableDatabase
val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy) return db.transactionally {
val bills: MutableList<DBBill> = ArrayList() val bills = queryAll(db, table_bills, columnsBills, selection, selectionArgs, orderBy, ::getBillFromCursor)
while (cursor.moveToNext()) { if (bills.isNotEmpty()) {
bills.add(getBillFromCursor(cursor)) val owersByBill = queryAll(
db, table_billowers, columnsBillowers,
"$key_billId IN (SELECT $key_id FROM $table_bills WHERE $selection)",
selectionArgs, null, ::getBillOwerFromCursor
).groupBy { it.billId }
for (bill in bills) {
bill.billOwers = owersByBill[bill.id].orEmpty()
}
}
bills
} }
cursor.close()
if (bills.isEmpty()) return bills
// Every ower of the matched bills in one query, keyed back to its bill.
//
// Fetching them per bill meant a project with thousands of bills issued thousands of
// queries on every list refresh - and that refresh runs on resume, on switching project,
// and after each sync. The bill ids are re-selected as a subquery rather than bound as
// arguments, which would blow past SQLite's limit on bound variables for a large project.
val owersByBill = getBillOwersCustom(
"$key_billId IN (SELECT $key_id FROM $table_bills WHERE $selection)",
selectionArgs,
null
).groupBy { it.billId }
for (bill in bills) {
bill.billOwers = owersByBill[bill.id].orEmpty()
}
return bills
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -714,14 +698,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getBillOwersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBillOwer> { private fun getBillOwersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBillOwer> {
val db = readableDatabase return queryAll(readableDatabase, table_billowers, columnsBillowers, selection, selectionArgs, orderBy, ::getBillOwerFromCursor)
val cursor = db.query(table_billowers, columnsBillowers, selection, selectionArgs, null, null, orderBy)
val billOwers: MutableList<DBBillOwer> = ArrayList()
while (cursor.moveToNext()) {
billOwers.add(getBillOwerFromCursor(cursor))
}
cursor.close()
return billOwers
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -788,14 +765,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCategory> { private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCategory> {
val db = readableDatabase return queryAll(readableDatabase, table_categories, columnsCategories, selection, selectionArgs, orderBy, ::getCategoryFromCursor)
val cursor = db.query(table_categories, columnsCategories, selection, selectionArgs, null, null, orderBy)
val categories: MutableList<DBCategory> = ArrayList()
while (cursor.moveToNext()) {
categories.add(getCategoryFromCursor(cursor))
}
cursor.close()
return categories
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -904,13 +874,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBPaymentMode> { private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBPaymentMode> {
val cursor = db.query(table_payment_modes, columnsPaymentModes, selection, selectionArgs, null, null, orderBy) return queryAll(db, table_payment_modes, columnsPaymentModes, selection, selectionArgs, orderBy, ::getPaymentModeFromCursor)
val paymentModes: MutableList<DBPaymentMode> = ArrayList()
while (cursor.moveToNext()) {
paymentModes.add(getPaymentModeFromCursor(cursor))
}
cursor.close()
return paymentModes
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -1020,13 +984,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCurrency> { private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCurrency> {
val cursor = db.query(table_currencies, columnsCurrencies, selection, selectionArgs, null, null, orderBy) return queryAll(db, table_currencies, columnsCurrencies, selection, selectionArgs, orderBy, ::getCurrencyFromCursor)
val currencies: MutableList<DBCurrency> = ArrayList()
while (cursor.moveToNext()) {
currencies.add(getCurrencyFromCursor(cursor))
}
cursor.close()
return currencies
} }
@SuppressLint("Range") @SuppressLint("Range")
@@ -1074,6 +1032,36 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
// --- Common Helpers --- // --- Common Helpers ---
@WorkerThread
private fun <T> queryAll(
db: SQLiteDatabase,
table: String,
columns: Array<String>,
selection: String?,
selectionArgs: Array<String>?,
orderBy: String?,
fromCursor: (Cursor) -> T
): List<T> = db.transactionally {
val items: MutableList<T> = ArrayList()
db.query(table, columns, selection, selectionArgs, null, null, orderBy).use { cursor ->
while (cursor.moveToNext()) {
items.add(fromCursor(cursor))
}
}
items
}
private fun <T> SQLiteDatabase.transactionally(block: () -> T): T {
beginTransaction()
try {
val result = block()
setTransactionSuccessful()
return result
} finally {
endTransaction()
}
}
fun syncIfRemote(proj: DBProject) { fun syncIfRemote(proj: DBProject) {
if (!proj.isLocal) { if (!proj.isLocal) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
@@ -1146,13 +1134,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
@WorkerThread @WorkerThread
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBAccountProject> { private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBAccountProject> {
val cursor = db.query(table_account_projects, columnsAccountProjects, selection, selectionArgs, null, null, orderBy) return queryAll(db, table_account_projects, columnsAccountProjects, selection, selectionArgs, orderBy, ::getAccountProjectFromCursor)
val accountProjects: MutableList<DBAccountProject> = ArrayList()
while (cursor.moveToNext()) {
accountProjects.add(getAccountProjectFromCursor(cursor))
}
cursor.close()
return accountProjects
} }
@SuppressLint("Range") @SuppressLint("Range")