From e21ea78f66d102842426d87e5f7872fe9e60f473 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sun, 19 Jan 2025 03:22:02 -0600 Subject: [PATCH] Remove temp table from recursive query --- .../java/com/todoroo/astrid/dao/TaskDao.kt | 5 +- .../java/org/tasks/data/TaskDaoExtensions.kt | 13 +- .../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 +++-- .../kotlin/org/tasks/data/TaskListQuery.kt | 33 ++-- .../tasks/data/TaskListQueryNonRecursive.kt | 11 +- .../org/tasks/data/TaskListQueryRecursive.kt | 112 +++++++----- 10 files changed, 130 insertions(+), 273 deletions(-) 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 9e50cffea..fedf2ca02 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt @@ -144,10 +144,9 @@ class TaskDao @Inject constructor( internal suspend fun insert(task: Task): Long = taskDao.insert(task) - internal suspend fun fetchTasks(callback: suspend () -> List): List = - taskDao.fetchTasks(callback) + internal suspend fun fetchTasks(query: String): List = taskDao.fetchTasks(query) 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 b78c5f745..cbee0f5e8 100644 --- a/app/src/main/java/org/tasks/data/TaskDaoExtensions.kt +++ b/app/src/main/java/org/tasks/data/TaskDaoExtensions.kt @@ -8,13 +8,9 @@ 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) @@ -27,18 +23,13 @@ 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) - val start = if (BuildConfig.DEBUG) currentTimeMillis() else 0 - val count = countRaw(query) - Timber.v("%sms: %s", currentTimeMillis() - start, query) - return count + return count(query) } private fun getQuery(queryTemplate: String, vararg fields: Field): String = diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt index 2c6c20095..ca3260e7f 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 73642360b..52bdd9d8e 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): List { + private suspend fun getQuery(filter: Filter): String { 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 bdff328fe..a8f39c1a8 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt @@ -1,176 +1,6 @@ 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 f056086d0..921f16e76 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, - val parentComplete: Boolean = false, + @ColumnInfo(name = "parent_complete") val parentComplete: Boolean = false, @ColumnInfo(name = "tags") val tagsString: String? = null, val children: Int = 0, - val sortGroup: Long? = null, - val primarySort: Long = 0, - val secondarySort: Long = 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, 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 62067773f..334578420 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt @@ -3,8 +3,9 @@ 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 @@ -14,11 +15,8 @@ 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 @@ -110,26 +108,27 @@ FROM ( + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''") abstract suspend fun clearCompletedCalendarEvents(): Int - 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 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 + } - suspend fun fetchTasks(query: String): List = - database.rawQuery(query) { it.getTasks() } + @RawQuery + internal abstract suspend fun fetchRaw(query: RoomRawQuery): List + + 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 countRaw(query: String): Int = - database.rawQuery(query) { if (it.step()) it.getInt(0) else 0 } + @RawQuery + internal abstract suspend fun countRaw(query: RoomRawQuery): Int suspend fun touch(ids: List, now: Long = DateTimeUtils2.currentTimeMillis()) = ids.eachChunk { internalTouch(it, now) } diff --git a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt index abafc372a..b43e110d0 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQuery.kt @@ -1,5 +1,6 @@ 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 @@ -7,6 +8,7 @@ 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 @@ -14,6 +16,7 @@ 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" @@ -21,11 +24,9 @@ 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))} @@ -41,13 +42,19 @@ object TaskListQuery { fun getQuery( preferences: QueryPreferences, filter: Filter, - ): 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) + ): 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" } } } -} \ 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 d7381393d..b298d30cf 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): MutableList { + fun getNonRecursiveQuery(filter: Filter, preferences: QueryPreferences): String { val joinedQuery = JOINS + if (filter is AstridOrderingFilter) filter.getSqlQuery() else filter.sql!! val sortMode = preferences.sortMode val groupMode = preferences.groupMode @@ -52,10 +52,9 @@ internal object TaskListQueryNonRecursive { else -> "$query GROUP BY ${Task.ID}" } - return mutableListOf( - Query.select(*FIELDS.plus(sortGroup)) - .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(groupedQuery)) - .from(Task.TABLE) - .toString()) + return 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 0047e1584..da28b4734 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryRecursive.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/data/TaskListQueryRecursive.kt @@ -5,12 +5,10 @@ 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 @@ -21,30 +19,24 @@ internal object TaskListQueryRecursive { 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("group_concat(distinct(tag_uid))").`as`("tags"), field("indent"), - field("sort_group").`as`("sortGroup"), + field("sort_group"), field("children"), - field("primary_sort").`as`("primarySort"), - field("secondary_sort").`as`("secondarySort"), - field("parent_complete").`as`("parentComplete"), + field("primary_sort"), + field("secondary_sort"), + field("parent_complete"), )).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()) + // TODO: switch to datastore, reading from preferences is expensive (30+ ms) fun getRecursiveQuery( filter: Filter, preferences: QueryPreferences, - ): MutableList { + ): String { val parentQuery = when (filter) { is CaldavFilter -> newCaldavQuery(filter.uuid) else -> PermaSql.replacePlaceholdersForQuery(filter.sql!!) @@ -74,48 +66,88 @@ 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 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) { + val completedAtBottom = preferences.completedTasksAtBottom + val parentCompleted = if (completedAtBottom) "tasks.completed > 0" else "0" + val completionSort = if (completedAtBottom) { "(CASE WHEN tasks.completed > 0 THEN ${SortHelper.orderSelectForSortTypeRecursive(completedMode, false)} ELSE 0 END)" } else { "0" } - 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)} + 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 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, 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 + 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 $SUBTASK_QUERY - 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) + 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 + ${FIELDS.joinToString(", ") { it.toStringInSelect() }} + 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 + ${PermaSql.replacePlaceholdersForQuery("${TaskListQuery.JOINS} WHERE recursive_tasks.hidden = 0")} + AND recursive_tasks.indent = max_indent + GROUP BY tasks._id + ORDER BY sequence """.trimIndent() - 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(), - ) + return SortHelper.adjustQueryForFlags(preferences, query) } private fun newCaldavQuery(list: String) =