From 06f31571a3aaf424fb02040c712a04a51c7c3cd1 Mon Sep 17 00:00:00 2001 From: soraefir Date: Thu, 10 Sep 2026 00:20:11 +0200 Subject: [PATCH] Fix db crash on big projects --- .../persistence/CowspentSQLiteOpenHelper.kt | 138 ++++++++---------- 1 file changed, 60 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt index 6ca4b8f..b3ffa6d 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt @@ -238,13 +238,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getProjectsCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { - val cursor = db.query(table_projects, columnsProjects, selection, selectionArgs, null, null, orderBy) - val projects: MutableList = ArrayList() - while (cursor.moveToNext()) { - projects.add(getProjectFromCursor(cursor)) - } - cursor.close() - return projects + return queryAll(db, table_projects, columnsProjects, selection, selectionArgs, orderBy, ::getProjectFromCursor) } @SuppressLint("Range") @@ -305,11 +299,17 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : fun deleteProject(id: Long) { val db = writableDatabase - for (b in getBillsOfProject(id)) { - deleteBill(b.id) + val projectId = arrayOf(id.toString()) + 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") } @@ -413,14 +413,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getMembersCustom(selection: String, selectionArgs: Array, orderBy: String?): List { - val db = readableDatabase - val cursor = db.query(table_members, columnsMembers, selection, selectionArgs, null, null, orderBy) - val members: MutableList = ArrayList() - while (cursor.moveToNext()) { - members.add(getMemberFromCursor(cursor)) - } - cursor.close() - return members + return queryAll(readableDatabase, table_members, columnsMembers, selection, selectionArgs, orderBy, ::getMemberFromCursor) } @SuppressLint("Range") @@ -644,29 +637,20 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getBillsCustom(selection: String, selectionArgs: Array, orderBy: String?): List { val db = readableDatabase - val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy) - val bills: MutableList = ArrayList() - while (cursor.moveToNext()) { - bills.add(getBillFromCursor(cursor)) + return db.transactionally { + val bills = queryAll(db, table_bills, columnsBills, selection, selectionArgs, orderBy, ::getBillFromCursor) + if (bills.isNotEmpty()) { + 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") @@ -714,14 +698,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getBillOwersCustom(selection: String, selectionArgs: Array, orderBy: String?): List { - val db = readableDatabase - val cursor = db.query(table_billowers, columnsBillowers, selection, selectionArgs, null, null, orderBy) - val billOwers: MutableList = ArrayList() - while (cursor.moveToNext()) { - billOwers.add(getBillOwerFromCursor(cursor)) - } - cursor.close() - return billOwers + return queryAll(readableDatabase, table_billowers, columnsBillowers, selection, selectionArgs, orderBy, ::getBillOwerFromCursor) } @SuppressLint("Range") @@ -788,14 +765,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getCategoriesCustom(selection: String, selectionArgs: Array, orderBy: String?): List { - val db = readableDatabase - val cursor = db.query(table_categories, columnsCategories, selection, selectionArgs, null, null, orderBy) - val categories: MutableList = ArrayList() - while (cursor.moveToNext()) { - categories.add(getCategoryFromCursor(cursor)) - } - cursor.close() - return categories + return queryAll(readableDatabase, table_categories, columnsCategories, selection, selectionArgs, orderBy, ::getCategoryFromCursor) } @SuppressLint("Range") @@ -904,13 +874,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getPaymentModesCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { - val cursor = db.query(table_payment_modes, columnsPaymentModes, selection, selectionArgs, null, null, orderBy) - val paymentModes: MutableList = ArrayList() - while (cursor.moveToNext()) { - paymentModes.add(getPaymentModeFromCursor(cursor)) - } - cursor.close() - return paymentModes + return queryAll(db, table_payment_modes, columnsPaymentModes, selection, selectionArgs, orderBy, ::getPaymentModeFromCursor) } @SuppressLint("Range") @@ -1020,13 +984,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getCurrenciesCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { - val cursor = db.query(table_currencies, columnsCurrencies, selection, selectionArgs, null, null, orderBy) - val currencies: MutableList = ArrayList() - while (cursor.moveToNext()) { - currencies.add(getCurrencyFromCursor(cursor)) - } - cursor.close() - return currencies + return queryAll(db, table_currencies, columnsCurrencies, selection, selectionArgs, orderBy, ::getCurrencyFromCursor) } @SuppressLint("Range") @@ -1074,6 +1032,36 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : // --- Common Helpers --- + @WorkerThread + private fun queryAll( + db: SQLiteDatabase, + table: String, + columns: Array, + selection: String?, + selectionArgs: Array?, + orderBy: String?, + fromCursor: (Cursor) -> T + ): List = db.transactionally { + val items: MutableList = ArrayList() + db.query(table, columns, selection, selectionArgs, null, null, orderBy).use { cursor -> + while (cursor.moveToNext()) { + items.add(fromCursor(cursor)) + } + } + items + } + + private fun SQLiteDatabase.transactionally(block: () -> T): T { + beginTransaction() + try { + val result = block() + setTransactionSuccessful() + return result + } finally { + endTransaction() + } + } + fun syncIfRemote(proj: DBProject) { if (!proj.isLocal) { val preferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -1146,13 +1134,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @WorkerThread private fun getAccountProjectsCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { - val cursor = db.query(table_account_projects, columnsAccountProjects, selection, selectionArgs, null, null, orderBy) - val accountProjects: MutableList = ArrayList() - while (cursor.moveToNext()) { - accountProjects.add(getAccountProjectFromCursor(cursor)) - } - cursor.close() - return accountProjects + return queryAll(db, table_account_projects, columnsAccountProjects, selection, selectionArgs, orderBy, ::getAccountProjectFromCursor) } @SuppressLint("Range")