From 274111f28600632256c142e40e056d26e1ff72f5 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 22 Jan 2025 00:13:03 -0600 Subject: [PATCH] Revert database improvements Too many database locked crashes https://issuetracker.google.com/issues/380088809 --- app/build.gradle.kts | 1 - .../CaldavManualSortTaskAdapterTest.kt | 2 +- .../GoogleTaskManualSortAdapterTest.kt | 2 +- .../astrid/adapter/OfflineSubtaskTest.kt | 2 +- .../astrid/adapter/RecursiveLoopTest.kt | 4 +- .../tasks/data/ManualGoogleTaskQueryTest.kt | 4 +- .../java/org/tasks/injection/TestModule.kt | 2 - .../java/com/todoroo/astrid/dao/TaskDao.kt | 5 +- .../java/org/tasks/data/TaskDaoExtensions.kt | 13 +- .../org/tasks/injection/ProductionModule.kt | 2 - .../java/org/tasks/ui/TaskListViewModel.kt | 2 +- .../tasks/widget/TasksWidgetViewFactory.kt | 4 +- .../tasks/data/SQLiteStatementExtensions.kt | 170 ++++++++++++++++++ .../kotlin/org/tasks/data/TaskContainer.kt | 10 +- .../kotlin/org/tasks/data/dao/TaskDao.kt | 43 ++--- deps_fdroid.txt | 9 - deps_googleplay.txt | 9 - gradle/libs.versions.toml | 1 - .../kotlin/org/tasks/data/TaskListQuery.kt | 33 ++-- .../tasks/data/TaskListQueryNonRecursive.kt | 11 +- .../org/tasks/data/TaskListQueryRecursive.kt | 132 +++++--------- 21 files changed, 287 insertions(+), 174 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffa74becc..c55826180 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -184,7 +184,6 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.room) - implementation(libs.androidx.sqlite) implementation(libs.androidx.appcompat) implementation(libs.iconics) implementation(libs.markwon) diff --git a/app/src/androidTest/java/com/todoroo/astrid/adapter/CaldavManualSortTaskAdapterTest.kt b/app/src/androidTest/java/com/todoroo/astrid/adapter/CaldavManualSortTaskAdapterTest.kt index 90a065329..a64fb4b02 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/adapter/CaldavManualSortTaskAdapterTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/adapter/CaldavManualSortTaskAdapterTest.kt @@ -223,7 +223,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() { } private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { - tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) + tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) }) val adjustedTo = if (from < to) to + 1 else to // match DragAndDropRecyclerAdapter behavior adapter.moved(from, adjustedTo, indent) } diff --git a/app/src/androidTest/java/com/todoroo/astrid/adapter/GoogleTaskManualSortAdapterTest.kt b/app/src/androidTest/java/com/todoroo/astrid/adapter/GoogleTaskManualSortAdapterTest.kt index 7f9ff91b9..eca69485c 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/adapter/GoogleTaskManualSortAdapterTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/adapter/GoogleTaskManualSortAdapterTest.kt @@ -426,7 +426,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() { } private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { - tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) + tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) }) val adjustedTo = if (from < to) to + 1 else to adapter.moved(from, adjustedTo, indent) } diff --git a/app/src/androidTest/java/com/todoroo/astrid/adapter/OfflineSubtaskTest.kt b/app/src/androidTest/java/com/todoroo/astrid/adapter/OfflineSubtaskTest.kt index ce10b0533..46a7dd543 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/adapter/OfflineSubtaskTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/adapter/OfflineSubtaskTest.kt @@ -84,6 +84,6 @@ class OfflineSubtaskTest : InjectingTestCase() { } private fun query() = runBlocking { - tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) + tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) }) } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/todoroo/astrid/adapter/RecursiveLoopTest.kt b/app/src/androidTest/java/com/todoroo/astrid/adapter/RecursiveLoopTest.kt index fbb4e5991..c363e7612 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/adapter/RecursiveLoopTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/adapter/RecursiveLoopTest.kt @@ -75,9 +75,9 @@ class RecursiveLoopTest : InjectingTestCase() { assertEquals(grandchild, tasks[2].id) } - private suspend fun getTasks() = taskDao.fetchTasks( + private suspend fun getTasks() = taskDao.fetchTasks { getQuery(preferences, TodayFilter.create()) - ) + } private suspend fun addTask(vararg properties: PropertyValue): Long { val task = newTask(*properties) diff --git a/app/src/androidTest/java/org/tasks/data/ManualGoogleTaskQueryTest.kt b/app/src/androidTest/java/org/tasks/data/ManualGoogleTaskQueryTest.kt index 17ecfbfe0..a52fbb450 100644 --- a/app/src/androidTest/java/org/tasks/data/ManualGoogleTaskQueryTest.kt +++ b/app/src/androidTest/java/org/tasks/data/ManualGoogleTaskQueryTest.kt @@ -105,7 +105,7 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() { googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.uuid), with(TASK, id))) } - private suspend fun query(): List = taskDao.fetchTasks( + private suspend fun query(): List = taskDao.fetchTasks { TaskListQuery.getQuery(preferences, filter) - ) + } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/tasks/injection/TestModule.kt b/app/src/androidTest/java/org/tasks/injection/TestModule.kt index b5b19d07d..148e3b8d9 100644 --- a/app/src/androidTest/java/org/tasks/injection/TestModule.kt +++ b/app/src/androidTest/java/org/tasks/injection/TestModule.kt @@ -2,7 +2,6 @@ package org.tasks.injection import android.content.Context import androidx.room.Room -import androidx.sqlite.driver.bundled.BundledSQLiteDriver import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -26,7 +25,6 @@ class TestModule { @Singleton fun getDatabase(@ApplicationContext context: Context): Database { return Room.inMemoryDatabaseBuilder(context, Database::class.java) - .setDriver(BundledSQLiteDriver()) .fallbackToDestructiveMigration(dropAllTables = true) .build() } diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt index fedf2ca02..9e50cffea 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt @@ -144,9 +144,10 @@ class TaskDao @Inject constructor( internal suspend fun insert(task: Task): Long = taskDao.insert(task) - internal suspend fun fetchTasks(query: String): List = taskDao.fetchTasks(query) + internal suspend fun fetchTasks(callback: suspend () -> List): List = + taskDao.fetchTasks(callback) internal suspend fun getAll(): List = taskDao.getAll() internal suspend fun getActiveTasks(): List = taskDao.getActiveTasks() -} +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/TaskDaoExtensions.kt b/app/src/main/java/org/tasks/data/TaskDaoExtensions.kt index cbee0f5e8..b78c5f745 100644 --- a/app/src/main/java/org/tasks/data/TaskDaoExtensions.kt +++ b/app/src/main/java/org/tasks/data/TaskDaoExtensions.kt @@ -8,9 +8,13 @@ import org.tasks.data.sql.Field import org.tasks.data.sql.Query import org.tasks.filters.Filter import org.tasks.preferences.QueryPreferences +import org.tasks.time.DateTimeUtils2.currentTimeMillis +import timber.log.Timber suspend fun TaskDao.fetchTasks(preferences: QueryPreferences, filter: Filter): List = - fetchTasks(TaskListQuery.getQuery(preferences, filter)) + fetchTasks { + TaskListQuery.getQuery(preferences, filter) + } internal suspend fun TaskDao.setCollapsed(preferences: QueryPreferences, filter: Filter, collapsed: Boolean) { fetchTasks(preferences, filter) @@ -23,13 +27,18 @@ suspend fun TaskDao.fetchFiltered(filter: Filter): List = fetchFiltered(fi suspend fun TaskDao.fetchFiltered(queryTemplate: String): List { val query = getQuery(queryTemplate, Task.FIELDS) + val start = if (BuildConfig.DEBUG) currentTimeMillis() else 0 val tasks = fetchTasks(query) + Timber.v("%sms: %s", currentTimeMillis() - start, query) return tasks.map(TaskContainer::task) } suspend fun TaskDao.count(filter: Filter): Int { val query = getQuery(filter.sql!!, Field.COUNT) - return count(query) + val start = if (BuildConfig.DEBUG) currentTimeMillis() else 0 + val count = countRaw(query) + Timber.v("%sms: %s", currentTimeMillis() - start, query) + return count } private fun getQuery(queryTemplate: String, vararg fields: Field): String = diff --git a/app/src/main/java/org/tasks/injection/ProductionModule.kt b/app/src/main/java/org/tasks/injection/ProductionModule.kt index 7de63a7bc..efe71de18 100644 --- a/app/src/main/java/org/tasks/injection/ProductionModule.kt +++ b/app/src/main/java/org/tasks/injection/ProductionModule.kt @@ -2,7 +2,6 @@ package org.tasks.injection import android.content.Context import androidx.room.Room -import androidx.sqlite.driver.bundled.BundledSQLiteDriver import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -37,7 +36,6 @@ internal class ProductionModule { context = context, name = databaseFile.absolutePath ) - .setDriver(BundledSQLiteDriver()) .addMigrations(*Migrations.migrations(context, fileStorage)) if (!BuildConfig.DEBUG || !preferences.getBoolean(R.string.p_crash_main_queries, false)) { builder.allowMainThreadQueries() diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt index ca3260e7f..2c6c20095 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt @@ -144,7 +144,7 @@ class TaskListViewModel @Inject constructor( it.searchQuery.isBlank() -> MyTasksFilter.create() else -> applicationContext.createSearchQuery(it.searchQuery) } - taskDao.fetchTasks(getQuery(preferences, filter)) + taskDao.fetchTasks { getQuery(preferences, filter) } } .onEach { tasks -> _state.update { diff --git a/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt b/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt index 52bdd9d8e..73642360b 100644 --- a/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt +++ b/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt @@ -70,7 +70,7 @@ internal class TasksWidgetViewFactory( runBlocking { val collapsed = widgetPreferences.collapsed tasks = SectionedDataSource( - taskDao.fetchTasks(getQuery(filter)), + taskDao.fetchTasks { getQuery(filter) }, disableGroups, settings.groupMode, widgetPreferences.subtaskMode, @@ -271,7 +271,7 @@ internal class TasksWidgetViewFactory( } } - private suspend fun getQuery(filter: Filter): String { + private suspend fun getQuery(filter: Filter): List { subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences) return getQuery(widgetPreferences, filter) } diff --git a/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt b/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt index a8f39c1a8..bdff328fe 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt @@ -1,6 +1,176 @@ package org.tasks.data +import androidx.room.util.getColumnIndex +import androidx.room.util.getColumnIndexOrThrow import androidx.sqlite.SQLiteStatement +import org.tasks.data.entity.CaldavTask +import org.tasks.data.entity.Geofence +import org.tasks.data.entity.Place +import org.tasks.data.entity.Task + +/* +room kmp doesn't support raw query yet 😢 +https://issuetracker.google.com/issues/330586815 + */ +fun SQLiteStatement.getTasks(): List { + val result = mutableListOf() + val _cursorIndexOfAccountType: Int = getColumnIndex(this, "accountType") + val _cursorIndexOfParentComplete: Int = getColumnIndex(this, "parentComplete") + val _cursorIndexOfTagsString: Int = getColumnIndex(this, "tags") + val _cursorIndexOfChildren: Int = getColumnIndex(this, "children") + val _cursorIndexOfSortGroup: Int = getColumnIndex(this, "sortGroup") + val _cursorIndexOfPrimarySort: Int = getColumnIndex(this, "primarySort") + val _cursorIndexOfSecondarySort: Int = getColumnIndex(this, "secondarySort") + val _cursorIndexOfIndent: Int = getColumnIndex(this, "indent") + val _cursorIndexOfId: Int = getColumnIndexOrThrow(this, "_id") + val _cursorIndexOfTitle: Int = getColumnIndexOrThrow(this, "title") + val _cursorIndexOfPriority: Int = getColumnIndexOrThrow(this, "importance") + val _cursorIndexOfDueDate: Int = getColumnIndexOrThrow(this, "dueDate") + val _cursorIndexOfHideUntil: Int = getColumnIndexOrThrow(this, "hideUntil") + val _cursorIndexOfCreationDate: Int = getColumnIndexOrThrow(this, "created") + val _cursorIndexOfModificationDate: Int = getColumnIndexOrThrow(this, "modified") + val _cursorIndexOfCompletionDate: Int = getColumnIndexOrThrow(this, "completed") + val _cursorIndexOfDeletionDate: Int = getColumnIndexOrThrow(this, "deleted") + val _cursorIndexOfNotes: Int = getColumnIndexOrThrow(this, "notes") + val _cursorIndexOfEstimatedSeconds: Int = getColumnIndexOrThrow(this, "estimatedSeconds") + val _cursorIndexOfElapsedSeconds: Int = getColumnIndexOrThrow(this, "elapsedSeconds") + val _cursorIndexOfTimerStart: Int = getColumnIndexOrThrow(this, "timerStart") + val _cursorIndexOfRingFlags: Int = getColumnIndexOrThrow(this, "notificationFlags") + val _cursorIndexOfReminderLast: Int = getColumnIndexOrThrow(this, "lastNotified") + val _cursorIndexOfRecurrence: Int = getColumnIndexOrThrow(this, "recurrence") + val _cursorIndexOfRepeatFrom: Int = getColumnIndexOrThrow(this, "repeat_from") + val _cursorIndexOfCalendarURI: Int = getColumnIndexOrThrow(this, "calendarUri") + val _cursorIndexOfRemoteId: Int = getColumnIndexOrThrow(this, "remoteId") + val _cursorIndexOfIsCollapsed: Int = getColumnIndexOrThrow(this, "collapsed") + val _cursorIndexOfParent: Int = getColumnIndexOrThrow(this, "parent") + val _cursorIndexOfOrder: Int = getColumnIndexOrThrow(this, "order") + val _cursorIndexOfReadOnly: Int = getColumnIndexOrThrow(this, "read_only") + val _cursorIndexOfId_1: Int = getColumnIndex(this, "cd_id") + val _cursorIndexOfTask: Int = getColumnIndex(this, "cd_task") + val _cursorIndexOfCalendar: Int = getColumnIndex(this, "cd_calendar") + val _cursorIndexOfRemoteId_1: Int = getColumnIndex(this, "cd_remote_id") + val _cursorIndexOfObj: Int = getColumnIndex(this, "cd_object") + val _cursorIndexOfEtag: Int = getColumnIndex(this, "cd_etag") + val _cursorIndexOfLastSync: Int = getColumnIndex(this, "cd_last_sync") + val _cursorIndexOfDeleted: Int = getColumnIndex(this, "cd_deleted") + val _cursorIndexOfRemoteParent: Int = getColumnIndex(this, "cd_remote_parent") + val _cursorIndexOfIsMoved: Int = getColumnIndex(this, "gt_moved") + val _cursorIndexOfRemoteOrder: Int = getColumnIndex(this, "gt_remote_order") + val _cursorIndexOfId_2: Int = getColumnIndex(this, "geofence_id") + val _cursorIndexOfTask_1: Int = getColumnIndex(this, "task") + val _cursorIndexOfPlace: Int = getColumnIndex(this, "place") + val _cursorIndexOfIsArrival: Int = getColumnIndex(this, "arrival") + val _cursorIndexOfIsDeparture: Int = getColumnIndex(this, "departure") + val _cursorIndexOfId_3: Int = getColumnIndex(this, "place_id") + val _cursorIndexOfUid: Int = getColumnIndex(this, "uid") + val _cursorIndexOfName: Int = getColumnIndex(this, "name") + val _cursorIndexOfAddress: Int = getColumnIndex(this, "address") + val _cursorIndexOfPhone: Int = getColumnIndex(this, "phone") + val _cursorIndexOfUrl: Int = getColumnIndex(this, "url") + val _cursorIndexOfLatitude: Int = getColumnIndex(this, "latitude") + val _cursorIndexOfLongitude: Int = getColumnIndex(this, "longitude") + val _cursorIndexOfColor: Int = getColumnIndex(this, "place_color") + val _cursorIndexOfIcon: Int = getColumnIndex(this, "place_icon") + val _cursorIndexOfOrder_1: Int = getColumnIndex(this, "place_order") + val _cursorIndexOfRadius: Int = getColumnIndex(this, "radius") + while (step()) { + val task = Task( + id = getLong(_cursorIndexOfId), + title = getTextOrNull(_cursorIndexOfTitle), + priority = getInt(_cursorIndexOfPriority), + dueDate = getLong(_cursorIndexOfDueDate), + hideUntil = getLong(_cursorIndexOfHideUntil), + creationDate = getLong(_cursorIndexOfCreationDate), + modificationDate = getLong(_cursorIndexOfModificationDate), + completionDate = getLong(_cursorIndexOfCompletionDate), + deletionDate = getLong(_cursorIndexOfDeletionDate), + notes = getTextOrNull(_cursorIndexOfNotes), + estimatedSeconds = getInt(_cursorIndexOfEstimatedSeconds), + elapsedSeconds = getInt(_cursorIndexOfElapsedSeconds), + timerStart = getLong(_cursorIndexOfTimerStart), + ringFlags = getInt(_cursorIndexOfRingFlags), + reminderLast = getLong(_cursorIndexOfReminderLast), + recurrence = getTextOrNull(_cursorIndexOfRecurrence), + repeatFrom = getInt(_cursorIndexOfRepeatFrom), + calendarURI = getTextOrNull(_cursorIndexOfCalendarURI), + remoteId = getTextOrNull(_cursorIndexOfRemoteId), + isCollapsed = getBoolean(_cursorIndexOfIsCollapsed), + parent = getLong(_cursorIndexOfParent), + order = getLongOrNull(_cursorIndexOfOrder), + readOnly = getBoolean(_cursorIndexOfReadOnly), + ) + val caldavTask = getLongOrNull(_cursorIndexOfId_1)?.takeIf { it > 0 }?.let { + CaldavTask( + id = it, + task = getLong(_cursorIndexOfTask), + calendar = getTextOrNull(_cursorIndexOfCalendar), + remoteId = getTextOrNull(_cursorIndexOfRemoteId_1), + obj = getTextOrNull(_cursorIndexOfObj), + etag = getTextOrNull(_cursorIndexOfEtag), + lastSync = getLong(_cursorIndexOfLastSync), + deleted = getLong(_cursorIndexOfDeleted), + remoteParent = getTextOrNull(_cursorIndexOfRemoteParent), + isMoved = getBoolean(_cursorIndexOfIsMoved), + remoteOrder = getLong(_cursorIndexOfRemoteOrder), + ) + } + val accountType = getIntOrNull(_cursorIndexOfAccountType) ?: 0 + val geofence = getLongOrNull(_cursorIndexOfId_2)?.takeIf { it > 0 }?.let { + Geofence( + id = it, + task = getLong(_cursorIndexOfTask_1), + place = getTextOrNull(_cursorIndexOfPlace), + isArrival = getBoolean(_cursorIndexOfIsArrival), + isDeparture = getBoolean(_cursorIndexOfIsDeparture), + ) + } + val place = getLongOrNull(_cursorIndexOfId_3)?.takeIf { it > 0 }?.let { + Place( + id = it, + uid = getTextOrNull(_cursorIndexOfUid), + name = getTextOrNull(_cursorIndexOfName), + address = getTextOrNull(_cursorIndexOfAddress), + phone = getTextOrNull(_cursorIndexOfPhone), + url = getTextOrNull(_cursorIndexOfUrl), + latitude = getDouble(_cursorIndexOfLatitude), + longitude = getDouble(_cursorIndexOfLongitude), + color = getInt(_cursorIndexOfColor), + icon = getTextOrNull(_cursorIndexOfIcon), + order = getInt(_cursorIndexOfOrder_1), + radius = getInt(_cursorIndexOfRadius), + ) + } + result.add( + TaskContainer( + task = task, + caldavTask = caldavTask, + accountType = accountType, + location = if (geofence != null && place != null) { + Location(geofence, place) + } else { + null + }, + tagsString = getTextOrNull(_cursorIndexOfTagsString), + indent = getIntOrNull(_cursorIndexOfIndent) ?: 0, + sortGroup = getLongOrNull(_cursorIndexOfSortGroup), + children = getIntOrNull(_cursorIndexOfChildren) ?: 0, + primarySort = getLongOrNull(_cursorIndexOfPrimarySort) ?: 0, + secondarySort = getLongOrNull(_cursorIndexOfSecondarySort) ?: 0, + parentComplete = getBooleanOrNull(_cursorIndexOfParentComplete) ?: false, + ) + ) + } + return result +} fun SQLiteStatement.getTextOrNull(index: Int): String? = if (index == -1 || isNull(index)) null else this.getText(index) + +private fun SQLiteStatement.getLongOrNull(index: Int): Long? = + if (index == -1 || isNull(index)) null else this.getLong(index) + +private fun SQLiteStatement.getIntOrNull(index: Int): Int? = + if (index == -1 || isNull(index)) null else this.getInt(index) + +private fun SQLiteStatement.getBooleanOrNull(index: Int): Boolean? = + if (index == -1 || isNull(index)) null else this.getBoolean(index) diff --git a/data/src/commonMain/kotlin/org/tasks/data/TaskContainer.kt b/data/src/commonMain/kotlin/org/tasks/data/TaskContainer.kt index 921f16e76..f056086d0 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/TaskContainer.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/TaskContainer.kt @@ -2,23 +2,23 @@ package org.tasks.data import androidx.room.ColumnInfo import androidx.room.Embedded +import org.tasks.data.entity.Task import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT import org.tasks.data.entity.CaldavTask -import org.tasks.data.entity.Task data class TaskContainer( @Embedded val task: Task, @Embedded val caldavTask: CaldavTask? = null, @Embedded val location: Location? = null, val accountType: Int = CaldavAccount.TYPE_LOCAL, - @ColumnInfo(name = "parent_complete") val parentComplete: Boolean = false, + val parentComplete: Boolean = false, @ColumnInfo(name = "tags") val tagsString: String? = null, val children: Int = 0, - @ColumnInfo(name = "sort_group") val sortGroup: Long? = null, - @ColumnInfo(name = "primary_sort") val primarySort: Long = 0, - @ColumnInfo(name = "secondary_sort") val secondarySort: Long = 0, + val sortGroup: Long? = null, + val primarySort: Long = 0, + val secondarySort: Long = 0, var indent: Int = 0, var targetIndent: Int = 0, ){ diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt index 334578420..62067773f 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt @@ -3,9 +3,8 @@ package org.tasks.data.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.Query -import androidx.room.RawQuery -import androidx.room.RoomRawQuery import androidx.room.Update +import androidx.room.execSQL import co.touchlab.kermit.Logger import org.tasks.IS_DEBUG import org.tasks.data.TaskContainer @@ -15,8 +14,11 @@ import org.tasks.data.db.SuspendDbUtils.chunkedMap import org.tasks.data.db.SuspendDbUtils.eachChunk import org.tasks.data.entity.Alarm import org.tasks.data.entity.Task +import org.tasks.data.getTasks +import org.tasks.data.rawQuery import org.tasks.data.sql.Criterion import org.tasks.data.sql.Functions +import org.tasks.data.withTransaction import org.tasks.time.DateTimeUtils2 private const val MAX_TIME = 9999999999999 @@ -108,27 +110,26 @@ FROM ( + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''") abstract suspend fun clearCompletedCalendarEvents(): Int - suspend fun fetchTasks(query: String): List { - val start = DateTimeUtils2.currentTimeMillis() - val result = fetchRaw(RoomRawQuery(query)) - val end = DateTimeUtils2.currentTimeMillis() - Logger.v("TaskDao") { "${end - start}ms: ${query.replace(Regex("\\s+"), " ").trim()}" } - return result - } - - @RawQuery - internal abstract suspend fun fetchRaw(query: RoomRawQuery): List + open suspend fun fetchTasks(callback: suspend () -> List): List = + database.withTransaction { + val start = if (IS_DEBUG) DateTimeUtils2.currentTimeMillis() else 0 + val queries = callback() + val last = queries.size - 1 + for (i in 0 until last) { + execSQL(queries[i]) + } + val result = usePrepared(queries[last]) { it.getTasks() } + Logger.v("TaskDao") { + "${DateTimeUtils2.currentTimeMillis() - start}ms: ${queries.joinToString(";\n")}" + } + result + } - suspend fun count(query: String): Int { - val start = DateTimeUtils2.currentTimeMillis() - val result = countRaw(RoomRawQuery(query)) - val end = DateTimeUtils2.currentTimeMillis() - Logger.v("TaskDao") { "${end - start}ms: ${query.replace(Regex("\\s+"), " ").trim()}" } - return result - } + suspend fun fetchTasks(query: String): List = + database.rawQuery(query) { it.getTasks() } - @RawQuery - internal abstract suspend fun countRaw(query: RoomRawQuery): Int + suspend fun countRaw(query: String): Int = + database.rawQuery(query) { if (it.step()) it.getInt(0) else 0 } suspend fun touch(ids: List, now: Long = DateTimeUtils2.currentTimeMillis()) = ids.eachChunk { internalTouch(it, now) } diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 63feb85b4..f55e127ea 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -247,7 +247,6 @@ +| | | +--- androidx.annotation:annotation:1.8.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 (c) -+| | | +--- androidx.sqlite:sqlite-bundled:2.5.0-alpha12 (c) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c) +| | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 +| | | \--- androidx.sqlite:sqlite-framework-android:2.5.0-alpha12 @@ -255,7 +254,6 @@ +| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (c) -+| | | +--- androidx.sqlite:sqlite-bundled:2.5.0-alpha12 (c) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c) +| | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | +--- org.jetbrains.kotlinx:atomicfu:0.17.0 -> 0.23.2 @@ -1175,13 +1173,6 @@ ++--- androidx.lifecycle:lifecycle-runtime-compose:2.8.7 (*) ++--- androidx.lifecycle:lifecycle-viewmodel:2.8.7 (*) ++--- androidx.room:room-runtime:2.7.0-alpha12 (*) -++--- androidx.sqlite:sqlite-bundled:2.5.0-alpha12 -+| \--- androidx.sqlite:sqlite-bundled-android:2.5.0-alpha12 -+| +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*) -+| +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) -+| +--- androidx.sqlite:sqlite:2.5.0-alpha12 (c) -+| +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 (c) -+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c) ++--- androidx.appcompat:appcompat:1.7.0 (*) ++--- com.mikepenz:iconics-core:5.5.0-b01 (*) ++--- io.noties.markwon:core:4.6.2 diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 9ecc17dd1..0291f638c 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -808,7 +808,6 @@ +| | | +--- androidx.annotation:annotation:1.8.1 -> 1.9.0 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 (c) -+| | | +--- androidx.sqlite:sqlite-bundled:2.5.0-alpha12 (c) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c) +| | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 +| | | \--- androidx.sqlite:sqlite-framework-android:2.5.0-alpha12 @@ -816,7 +815,6 @@ +| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (c) -+| | | +--- androidx.sqlite:sqlite-bundled:2.5.0-alpha12 (c) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c) +| | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | +--- org.jetbrains.kotlinx:atomicfu:0.17.0 -> 0.23.2 @@ -1524,13 +1522,6 @@ ++--- androidx.lifecycle:lifecycle-runtime-compose:2.8.7 (*) ++--- androidx.lifecycle:lifecycle-viewmodel:2.8.7 (*) ++--- androidx.room:room-runtime:2.7.0-alpha12 (*) -++--- androidx.sqlite:sqlite-bundled:2.5.0-alpha12 -+| \--- androidx.sqlite:sqlite-bundled-android:2.5.0-alpha12 -+| +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*) -+| +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) -+| +--- androidx.sqlite:sqlite:2.5.0-alpha12 (c) -+| +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 (c) -+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c) ++--- androidx.appcompat:appcompat:1.7.0 (*) ++--- com.mikepenz:iconics-core:5.5.0-b01 (*) ++--- io.noties.markwon:core:4.6.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd326f542..d8c7168a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,6 @@ androidx-preference = { module = "androidx.preference:preference", version.ref = androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room"} -androidx-sqlite = { module = "androidx.sqlite:sqlite-bundled", version = "2.5.0-alpha12" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } diff --git a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt index b43e110d0..abafc372a 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt @@ -1,6 +1,5 @@ package org.tasks.data -import co.touchlab.kermit.Logger import org.tasks.data.TaskListQueryNonRecursive.getNonRecursiveQuery import org.tasks.data.TaskListQueryRecursive.getRecursiveQuery import org.tasks.data.entity.CaldavAccount @@ -8,7 +7,6 @@ import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.Geofence import org.tasks.data.entity.Place -import org.tasks.data.entity.Tag import org.tasks.data.entity.Task import org.tasks.data.sql.Criterion import org.tasks.data.sql.Field.Companion.field @@ -16,7 +14,6 @@ import org.tasks.data.sql.Join import org.tasks.filters.AstridOrderingFilter import org.tasks.filters.Filter import org.tasks.preferences.QueryPreferences -import org.tasks.time.DateTimeUtils2.currentTimeMillis object TaskListQuery { private const val CALDAV_METADATA_JOIN = "for_caldav" @@ -24,9 +21,11 @@ object TaskListQuery { Task.ID.eq(field("$CALDAV_METADATA_JOIN.cd_task")), field("$CALDAV_METADATA_JOIN.cd_deleted").eq(0)) val JOINS = """ - ${Join.left(Tag.TABLE, Tag.TASK.eq(Task.ID))} ${Join.left(CaldavTask.TABLE.`as`(CALDAV_METADATA_JOIN), JOIN_CALDAV)} - ${Join.left(CaldavCalendar.TABLE, field("$CALDAV_METADATA_JOIN.cd_calendar").eq(CaldavCalendar.UUID))} + ${ + Join.left( + CaldavCalendar.TABLE, field("$CALDAV_METADATA_JOIN.cd_calendar").eq( + CaldavCalendar.UUID))} ${Join.left(CaldavAccount.TABLE, CaldavCalendar.ACCOUNT.eq(CaldavAccount.UUID))} ${Join.left(Geofence.TABLE, Geofence.TASK.eq(Task.ID))} ${Join.left(Place.TABLE, Place.UID.eq(Geofence.PLACE))} @@ -42,19 +41,13 @@ object TaskListQuery { fun getQuery( preferences: QueryPreferences, filter: Filter, - ): String { - val start = currentTimeMillis() - return when { - filter.supportsManualSort() && preferences.isManualSort -> - getRecursiveQuery(filter, preferences) - - filter is AstridOrderingFilter && preferences.isAstridSort -> - getNonRecursiveQuery(filter, preferences) - - filter.supportsSorting() -> - getRecursiveQuery(filter, preferences) - - else -> getNonRecursiveQuery(filter, preferences) - }.also { Logger.v { "Building query took ${currentTimeMillis() - start}ms" } } + ): MutableList = when { + filter.supportsManualSort() && preferences.isManualSort -> + getRecursiveQuery(filter, preferences) + filter is AstridOrderingFilter && preferences.isAstridSort -> + getNonRecursiveQuery(filter, preferences) + filter.supportsSorting() -> + getRecursiveQuery(filter, preferences) + else -> getNonRecursiveQuery(filter, preferences) } -} +} \ No newline at end of file diff --git a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryNonRecursive.kt b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryNonRecursive.kt index b298d30cf..d7381393d 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryNonRecursive.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryNonRecursive.kt @@ -29,7 +29,7 @@ internal object TaskListQueryNonRecursive { field("tasks.completed > 0").`as`("parentComplete") )).toTypedArray() - fun getNonRecursiveQuery(filter: Filter, preferences: QueryPreferences): String { + fun getNonRecursiveQuery(filter: Filter, preferences: QueryPreferences): MutableList { val joinedQuery = JOINS + if (filter is AstridOrderingFilter) filter.getSqlQuery() else filter.sql!! val sortMode = preferences.sortMode val groupMode = preferences.groupMode @@ -52,9 +52,10 @@ internal object TaskListQueryNonRecursive { else -> "$query GROUP BY ${Task.ID}" } - return Query.select(*FIELDS.plus(sortGroup)) - .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(groupedQuery)) - .from(Task.TABLE) - .toString() + return mutableListOf( + Query.select(*FIELDS.plus(sortGroup)) + .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(groupedQuery)) + .from(Task.TABLE) + .toString()) } } diff --git a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryRecursive.kt b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryRecursive.kt index e7b0cc480..0047e1584 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryRecursive.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryRecursive.kt @@ -5,10 +5,12 @@ import com.todoroo.astrid.core.SortHelper import org.tasks.data.dao.TaskDao.TaskCriteria.activeAndVisible import org.tasks.data.db.Table import org.tasks.data.entity.CaldavTask +import org.tasks.data.entity.Tag import org.tasks.data.entity.Task import org.tasks.data.sql.Criterion import org.tasks.data.sql.Field.Companion.field import org.tasks.data.sql.Join +import org.tasks.data.sql.Query import org.tasks.data.sql.QueryTemplate import org.tasks.filters.CaldavFilter import org.tasks.filters.Filter @@ -17,17 +19,32 @@ import org.tasks.preferences.QueryPreferences internal object TaskListQueryRecursive { private val RECURSIVE = Table("recursive_tasks") private val RECURSIVE_TASK = field("$RECURSIVE.task") + private val FIELDS = + TaskListQuery.FIELDS.plus(listOf( + field("(${ + Query.select(field("group_concat(distinct(tag_uid))")).from(Tag.TABLE).where( + Task.ID.eq(Tag.TASK))} GROUP BY ${Tag.TASK})").`as`("tags"), + field("indent"), + field("sort_group").`as`("sortGroup"), + field("children"), + field("primary_sort").`as`("primarySort"), + field("secondary_sort").`as`("secondarySort"), + field("parent_complete").`as`("parentComplete"), + )).toTypedArray() + private val JOINS = """ + ${Join.inner(RECURSIVE, Task.ID.eq(RECURSIVE_TASK))} + LEFT JOIN (SELECT parent, count(distinct recursive_tasks.task) AS children FROM recursive_tasks GROUP BY parent) AS recursive_children ON recursive_children.parent = tasks._id + ${TaskListQuery.JOINS} + """.trimIndent() private val SUBTASK_QUERY = QueryTemplate() - .join(Join.inner(RECURSIVE, Task.PARENT.eq(RECURSIVE_TASK))) - .where(activeAndVisible()) - .toString() + .join(Join.inner(RECURSIVE, Task.PARENT.eq(RECURSIVE_TASK))) + .where(activeAndVisible()) - // TODO: switch to datastore, reading from preferences is expensive (30+ ms) fun getRecursiveQuery( filter: Filter, preferences: QueryPreferences, - ): String { + ): MutableList { val parentQuery = when (filter) { is CaldavFilter -> newCaldavQuery(filter.uuid) else -> PermaSql.replacePlaceholdersForQuery(filter.sql!!) @@ -57,103 +74,48 @@ internal object TaskListQueryRecursive { preferences.sortAscending && sortMode != SortHelper.SORT_GTASKS && sortMode != SortHelper.SORT_CALDAV val subtaskAscending = preferences.subtaskAscending && subtaskMode != SortHelper.SORT_GTASKS && subtaskMode != SortHelper.SORT_CALDAV - val completedAtBottom = preferences.completedTasksAtBottom - val parentCompleted = if (completedAtBottom) "tasks.completed > 0" else "0" - val completionSort = if (completedAtBottom) { + val primaryGroupSelector = SortHelper.orderSelectForSortTypeRecursive(groupMode, true) + val primarySortSelect = SortHelper.orderSelectForSortTypeRecursive(sortMode, false) + val subtaskSort = SortHelper.orderSelectForSortTypeRecursive(subtaskMode, false) + val parentCompleted = if (preferences.completedTasksAtBottom) "tasks.completed > 0" else "0" + val completionSort = if (preferences.completedTasksAtBottom) { "(CASE WHEN tasks.completed > 0 THEN ${SortHelper.orderSelectForSortTypeRecursive(completedMode, false)} ELSE 0 END)" } else { "0" } - val query = """ - WITH RECURSIVE recursive_tasks AS ( - SELECT - tasks._id AS task, - $parentCompleted AS parent_complete, - 0 AS subtask_complete, - $completionSort AS completion_sort, - 0 AS parent, - tasks.collapsed AS collapsed, - 0 AS hidden, - 0 AS indent, - UPPER(tasks.title) AS sort_title, - ${SortHelper.orderSelectForSortTypeRecursive(groupMode, true)} AS primary_group, - ${SortHelper.orderSelectForSortTypeRecursive(sortMode, false)} AS primary_sort, - NULL as secondary_sort, - ${SortHelper.getSortGroup(groupMode)} AS sort_group + val withClause = """ + CREATE TEMPORARY TABLE `recursive_tasks` AS + WITH RECURSIVE recursive_tasks (task, parent_complete, subtask_complete, completion_sort, parent, collapsed, hidden, indent, title, primary_group, primary_sort, secondary_sort, sort_group) AS ( + SELECT tasks._id, $parentCompleted as parent_complete, 0 as subtask_complete, $completionSort as completion_sort, 0 as parent, tasks.collapsed as collapsed, 0 as hidden, 0 AS sort_indent, UPPER(tasks.title) AS sort_title, $primaryGroupSelector as primary_group, $primarySortSelect as primary_sort, NULL as secondarySort, ${SortHelper.getSortGroup(groupMode)} FROM tasks ${ if (groupMode == SortHelper.SORT_LIST) { """ - INNER JOIN caldav_tasks ON cd_task = tasks._id AND cd_deleted = 0 - INNER JOIN caldav_lists ON cd_calendar = cdl_uuid + INNER JOIN caldav_tasks on cd_task = tasks._id AND cd_deleted = 0 + INNER JOIN caldav_lists on cd_calendar = cdl_uuid """.trimIndent() } else { "" } } $parentQuery - UNION ALL SELECT - tasks._id AS task, - recursive_tasks.parent_complete AS parent_complete, - $parentCompleted AS subtask_complete, - $completionSort AS completion_sort, - recursive_tasks.task AS parent, - tasks.collapsed AS collapsed, - CASE WHEN recursive_tasks.collapsed > 0 OR recursive_tasks.hidden > 0 THEN 1 ELSE 0 END AS hidden, - recursive_tasks.indent+1 AS indent, - UPPER(tasks.title) AS sort_title, - recursive_tasks.primary_group AS primary_group, - recursive_tasks.primary_sort AS primary_sort, - ${SortHelper.orderSelectForSortTypeRecursive(subtaskMode, false)} AS secondary_sort, - recursive_tasks.sort_group AS sort_group - FROM tasks + UNION ALL SELECT tasks._id, recursive_tasks.parent_complete, $parentCompleted as subtask_complete, $completionSort as completion_sort, recursive_tasks.task as parent, tasks.collapsed as collapsed, CASE WHEN recursive_tasks.collapsed > 0 OR recursive_tasks.hidden > 0 THEN 1 ELSE 0 END as hidden, recursive_tasks.indent+1 AS sort_indent, UPPER(tasks.title) AS sort_title, recursive_tasks.primary_group as primary_group, recursive_tasks.primary_sort as primary_sort, $subtaskSort as secondary_sort, recursive_tasks.sort_group FROM tasks $SUBTASK_QUERY - ORDER BY - parent_complete, - indent DESC, - subtask_complete, - completion_sort ${if (preferences.completedAscending) "" else "DESC"}, - ${SortHelper.orderForGroupTypeRecursive(groupMode, groupAscending)}, - ${SortHelper.orderForSortTypeRecursive(sortMode, sortAscending, subtaskMode, subtaskAscending)} - ), - numbered_tasks AS ( - SELECT task, ROW_NUMBER() OVER () AS sequence - FROM recursive_tasks - ), - max_indent AS ( - SELECT task, - MAX(recursive_tasks.indent) OVER (PARTITION BY task) AS max_indent - FROM recursive_tasks - ), - child_counts AS ( - SELECT DISTINCT(parent), - COUNT(*) OVER (PARTITION BY parent) AS children - FROM recursive_tasks - WHERE parent > 0 - ) - SELECT - ${TaskListQuery.FIELDS.joinToString(",\n") { it.toStringInSelect() }}, - group_concat(distinct(tag_uid)) AS tags, - indent, - sort_group, - children, - primary_sort, - secondary_sort, - parent_complete - FROM tasks - INNER JOIN numbered_tasks ON tasks._id = numbered_tasks.task - INNER JOIN max_indent ON tasks._id = max_indent.task - INNER JOIN recursive_tasks ON recursive_tasks.task = tasks._id - LEFT JOIN child_counts ON child_counts.parent = tasks._id - ${TaskListQuery.JOINS} - WHERE - recursive_tasks.hidden = 0 - AND recursive_tasks.indent = max_indent - GROUP BY tasks._id - ORDER BY sequence + ORDER BY parent_complete ASC, sort_indent DESC, subtask_complete ASC, completion_sort ${if (preferences.completedAscending) "ASC" else "DESC"}, ${SortHelper.orderForGroupTypeRecursive(groupMode, groupAscending)}, ${SortHelper.orderForSortTypeRecursive(sortMode, sortAscending, subtaskMode, subtaskAscending)} + ) SELECT * FROM recursive_tasks + WHERE indent = (SELECT MAX(indent) FROM recursive_tasks as r WHERE r.task = recursive_tasks.task) """.trimIndent() - return SortHelper.adjustQueryForFlags(preferences, query) + return mutableListOf( + "DROP TABLE IF EXISTS `recursive_tasks`", + SortHelper.adjustQueryForFlags(preferences, withClause), + "CREATE INDEX `r_tasks` ON `recursive_tasks` (`task`)", + "CREATE INDEX `r_parents` ON `recursive_tasks` (`parent`)", + Query.select(*FIELDS) + .withQueryTemplate(PermaSql.replacePlaceholdersForQuery("$JOINS WHERE recursive_tasks.hidden = 0")) + .from(Task.TABLE) + .toString(), + ) } private fun newCaldavQuery(list: String) =