Revert database improvements

Too many database locked crashes
https://issuetracker.google.com/issues/380088809
pull/3289/head
Alex Baker 11 months ago
parent 828c5872b3
commit 274111f286

@ -184,7 +184,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room) implementation(libs.androidx.room)
implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.iconics) implementation(libs.iconics)
implementation(libs.markwon) implementation(libs.markwon)

@ -223,7 +223,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { 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 val adjustedTo = if (from < to) to + 1 else to // match DragAndDropRecyclerAdapter behavior
adapter.moved(from, adjustedTo, indent) adapter.moved(from, adjustedTo, indent)
} }

@ -426,7 +426,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { 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 val adjustedTo = if (from < to) to + 1 else to
adapter.moved(from, adjustedTo, indent) adapter.moved(from, adjustedTo, indent)
} }

@ -84,6 +84,6 @@ class OfflineSubtaskTest : InjectingTestCase() {
} }
private fun query() = runBlocking { private fun query() = runBlocking {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
} }
} }

@ -75,9 +75,9 @@ class RecursiveLoopTest : InjectingTestCase() {
assertEquals(grandchild, tasks[2].id) assertEquals(grandchild, tasks[2].id)
} }
private suspend fun getTasks() = taskDao.fetchTasks( private suspend fun getTasks() = taskDao.fetchTasks {
getQuery(preferences, TodayFilter.create()) getQuery(preferences, TodayFilter.create())
) }
private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long { private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
val task = newTask(*properties) val task = newTask(*properties)

@ -105,7 +105,7 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.uuid), with(TASK, id))) googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.uuid), with(TASK, id)))
} }
private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks( private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks {
TaskListQuery.getQuery(preferences, filter) TaskListQuery.getQuery(preferences, filter)
) }
} }

@ -2,7 +2,6 @@ package org.tasks.injection
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -26,7 +25,6 @@ class TestModule {
@Singleton @Singleton
fun getDatabase(@ApplicationContext context: Context): Database { fun getDatabase(@ApplicationContext context: Context): Database {
return Room.inMemoryDatabaseBuilder(context, Database::class.java) return Room.inMemoryDatabaseBuilder(context, Database::class.java)
.setDriver(BundledSQLiteDriver())
.fallbackToDestructiveMigration(dropAllTables = true) .fallbackToDestructiveMigration(dropAllTables = true)
.build() .build()
} }

@ -144,9 +144,10 @@ class TaskDao @Inject constructor(
internal suspend fun insert(task: Task): Long = taskDao.insert(task) internal suspend fun insert(task: Task): Long = taskDao.insert(task)
internal suspend fun fetchTasks(query: String): List<TaskContainer> = taskDao.fetchTasks(query) internal suspend fun fetchTasks(callback: suspend () -> List<String>): List<TaskContainer> =
taskDao.fetchTasks(callback)
internal suspend fun getAll(): List<Task> = taskDao.getAll() internal suspend fun getAll(): List<Task> = taskDao.getAll()
internal suspend fun getActiveTasks(): List<Task> = taskDao.getActiveTasks() internal suspend fun getActiveTasks(): List<Task> = taskDao.getActiveTasks()
} }

@ -8,9 +8,13 @@ import org.tasks.data.sql.Field
import org.tasks.data.sql.Query import org.tasks.data.sql.Query
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.preferences.QueryPreferences import org.tasks.preferences.QueryPreferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
suspend fun TaskDao.fetchTasks(preferences: QueryPreferences, filter: Filter): List<TaskContainer> = suspend fun TaskDao.fetchTasks(preferences: QueryPreferences, filter: Filter): List<TaskContainer> =
fetchTasks(TaskListQuery.getQuery(preferences, filter)) fetchTasks {
TaskListQuery.getQuery(preferences, filter)
}
internal suspend fun TaskDao.setCollapsed(preferences: QueryPreferences, filter: Filter, collapsed: Boolean) { internal suspend fun TaskDao.setCollapsed(preferences: QueryPreferences, filter: Filter, collapsed: Boolean) {
fetchTasks(preferences, filter) fetchTasks(preferences, filter)
@ -23,13 +27,18 @@ suspend fun TaskDao.fetchFiltered(filter: Filter): List<Task> = fetchFiltered(fi
suspend fun TaskDao.fetchFiltered(queryTemplate: String): List<Task> { suspend fun TaskDao.fetchFiltered(queryTemplate: String): List<Task> {
val query = getQuery(queryTemplate, Task.FIELDS) val query = getQuery(queryTemplate, Task.FIELDS)
val start = if (BuildConfig.DEBUG) currentTimeMillis() else 0
val tasks = fetchTasks(query) val tasks = fetchTasks(query)
Timber.v("%sms: %s", currentTimeMillis() - start, query)
return tasks.map(TaskContainer::task) return tasks.map(TaskContainer::task)
} }
suspend fun TaskDao.count(filter: Filter): Int { suspend fun TaskDao.count(filter: Filter): Int {
val query = getQuery(filter.sql!!, Field.COUNT) 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 = private fun getQuery(queryTemplate: String, vararg fields: Field): String =

@ -2,7 +2,6 @@ package org.tasks.injection
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -37,7 +36,6 @@ internal class ProductionModule {
context = context, context = context,
name = databaseFile.absolutePath name = databaseFile.absolutePath
) )
.setDriver(BundledSQLiteDriver())
.addMigrations(*Migrations.migrations(context, fileStorage)) .addMigrations(*Migrations.migrations(context, fileStorage))
if (!BuildConfig.DEBUG || !preferences.getBoolean(R.string.p_crash_main_queries, false)) { if (!BuildConfig.DEBUG || !preferences.getBoolean(R.string.p_crash_main_queries, false)) {
builder.allowMainThreadQueries() builder.allowMainThreadQueries()

@ -144,7 +144,7 @@ class TaskListViewModel @Inject constructor(
it.searchQuery.isBlank() -> MyTasksFilter.create() it.searchQuery.isBlank() -> MyTasksFilter.create()
else -> applicationContext.createSearchQuery(it.searchQuery) else -> applicationContext.createSearchQuery(it.searchQuery)
} }
taskDao.fetchTasks(getQuery(preferences, filter)) taskDao.fetchTasks { getQuery(preferences, filter) }
} }
.onEach { tasks -> .onEach { tasks ->
_state.update { _state.update {

@ -70,7 +70,7 @@ internal class TasksWidgetViewFactory(
runBlocking { runBlocking {
val collapsed = widgetPreferences.collapsed val collapsed = widgetPreferences.collapsed
tasks = SectionedDataSource( tasks = SectionedDataSource(
taskDao.fetchTasks(getQuery(filter)), taskDao.fetchTasks { getQuery(filter) },
disableGroups, disableGroups,
settings.groupMode, settings.groupMode,
widgetPreferences.subtaskMode, widgetPreferences.subtaskMode,
@ -271,7 +271,7 @@ internal class TasksWidgetViewFactory(
} }
} }
private suspend fun getQuery(filter: Filter): String { private suspend fun getQuery(filter: Filter): List<String> {
subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences) subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences)
return getQuery(widgetPreferences, filter) return getQuery(widgetPreferences, filter)
} }

@ -1,6 +1,176 @@
package org.tasks.data package org.tasks.data
import androidx.room.util.getColumnIndex
import androidx.room.util.getColumnIndexOrThrow
import androidx.sqlite.SQLiteStatement 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<TaskContainer> {
val result = mutableListOf<TaskContainer>()
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? = fun SQLiteStatement.getTextOrNull(index: Int): String? =
if (index == -1 || isNull(index)) null else this.getText(index) 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)

@ -2,23 +2,23 @@ package org.tasks.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import org.tasks.data.entity.Task
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Task
data class TaskContainer( data class TaskContainer(
@Embedded val task: Task, @Embedded val task: Task,
@Embedded val caldavTask: CaldavTask? = null, @Embedded val caldavTask: CaldavTask? = null,
@Embedded val location: Location? = null, @Embedded val location: Location? = null,
val accountType: Int = CaldavAccount.TYPE_LOCAL, 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, @ColumnInfo(name = "tags") val tagsString: String? = null,
val children: Int = 0, val children: Int = 0,
@ColumnInfo(name = "sort_group") val sortGroup: Long? = null, val sortGroup: Long? = null,
@ColumnInfo(name = "primary_sort") val primarySort: Long = 0, val primarySort: Long = 0,
@ColumnInfo(name = "secondary_sort") val secondarySort: Long = 0, val secondarySort: Long = 0,
var indent: Int = 0, var indent: Int = 0,
var targetIndent: Int = 0, var targetIndent: Int = 0,
){ ){

@ -3,9 +3,8 @@ package org.tasks.data.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.RoomRawQuery
import androidx.room.Update import androidx.room.Update
import androidx.room.execSQL
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import org.tasks.IS_DEBUG import org.tasks.IS_DEBUG
import org.tasks.data.TaskContainer 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.db.SuspendDbUtils.eachChunk
import org.tasks.data.entity.Alarm import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Task 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.Criterion
import org.tasks.data.sql.Functions import org.tasks.data.sql.Functions
import org.tasks.data.withTransaction
import org.tasks.time.DateTimeUtils2 import org.tasks.time.DateTimeUtils2
private const val MAX_TIME = 9999999999999 private const val MAX_TIME = 9999999999999
@ -108,27 +110,26 @@ FROM (
+ "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''") + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''")
abstract suspend fun clearCompletedCalendarEvents(): Int abstract suspend fun clearCompletedCalendarEvents(): Int
suspend fun fetchTasks(query: String): List<TaskContainer> { open suspend fun fetchTasks(callback: suspend () -> List<String>): List<TaskContainer> =
val start = DateTimeUtils2.currentTimeMillis() database.withTransaction {
val result = fetchRaw(RoomRawQuery(query)) val start = if (IS_DEBUG) DateTimeUtils2.currentTimeMillis() else 0
val end = DateTimeUtils2.currentTimeMillis() val queries = callback()
Logger.v("TaskDao") { "${end - start}ms: ${query.replace(Regex("\\s+"), " ").trim()}" } val last = queries.size - 1
return result for (i in 0 until last) {
} execSQL(queries[i])
}
@RawQuery val result = usePrepared(queries[last]) { it.getTasks() }
internal abstract suspend fun fetchRaw(query: RoomRawQuery): List<TaskContainer> Logger.v("TaskDao") {
"${DateTimeUtils2.currentTimeMillis() - start}ms: ${queries.joinToString(";\n")}"
}
result
}
suspend fun count(query: String): Int { suspend fun fetchTasks(query: String): List<TaskContainer> =
val start = DateTimeUtils2.currentTimeMillis() database.rawQuery(query) { it.getTasks() }
val result = countRaw(RoomRawQuery(query))
val end = DateTimeUtils2.currentTimeMillis()
Logger.v("TaskDao") { "${end - start}ms: ${query.replace(Regex("\\s+"), " ").trim()}" }
return result
}
@RawQuery suspend fun countRaw(query: String): Int =
internal abstract suspend fun countRaw(query: RoomRawQuery): Int database.rawQuery(query) { if (it.step()) it.getInt(0) else 0 }
suspend fun touch(ids: List<Long>, now: Long = DateTimeUtils2.currentTimeMillis()) = suspend fun touch(ids: List<Long>, now: Long = DateTimeUtils2.currentTimeMillis()) =
ids.eachChunk { internalTouch(it, now) } ids.eachChunk { internalTouch(it, now) }

@ -247,7 +247,6 @@
+| | | +--- androidx.annotation:annotation:1.8.1 (*) +| | | +--- androidx.annotation:annotation:1.8.1 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*)
+| | | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 (c) +| | | +--- 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) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c)
+| | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 +| | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12
+| | | \--- androidx.sqlite:sqlite-framework-android:2.5.0-alpha12 +| | | \--- androidx.sqlite:sqlite-framework-android:2.5.0-alpha12
@ -255,7 +254,6 @@
+| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*) +| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*)
+| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (c) +| | | +--- 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:1.8.22 -> 2.1.0 (c)
+| | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*)
+| | +--- org.jetbrains.kotlinx:atomicfu:0.17.0 -> 0.23.2 +| | +--- 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-runtime-compose:2.8.7 (*)
++--- androidx.lifecycle:lifecycle-viewmodel:2.8.7 (*) ++--- androidx.lifecycle:lifecycle-viewmodel:2.8.7 (*)
++--- androidx.room:room-runtime:2.7.0-alpha12 (*) ++--- 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 (*) ++--- androidx.appcompat:appcompat:1.7.0 (*)
++--- com.mikepenz:iconics-core:5.5.0-b01 (*) ++--- com.mikepenz:iconics-core:5.5.0-b01 (*)
++--- io.noties.markwon:core:4.6.2 ++--- io.noties.markwon:core:4.6.2

@ -808,7 +808,6 @@
+| | | +--- androidx.annotation:annotation:1.8.1 -> 1.9.0 (*) +| | | +--- androidx.annotation:annotation:1.8.1 -> 1.9.0 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*)
+| | | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 (c) +| | | +--- 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) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.0 (c)
+| | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12 +| | +--- androidx.sqlite:sqlite-framework:2.5.0-alpha12
+| | | \--- androidx.sqlite:sqlite-framework-android:2.5.0-alpha12 +| | | \--- androidx.sqlite:sqlite-framework-android:2.5.0-alpha12
@ -816,7 +815,6 @@
+| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*) +| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*)
+| | | +--- androidx.sqlite:sqlite:2.5.0-alpha12 (c) +| | | +--- 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:1.8.22 -> 2.1.0 (c)
+| | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib -> 2.1.0 (*)
+| | +--- org.jetbrains.kotlinx:atomicfu:0.17.0 -> 0.23.2 +| | +--- 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-runtime-compose:2.8.7 (*)
++--- androidx.lifecycle:lifecycle-viewmodel:2.8.7 (*) ++--- androidx.lifecycle:lifecycle-viewmodel:2.8.7 (*)
++--- androidx.room:room-runtime:2.7.0-alpha12 (*) ++--- 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 (*) ++--- androidx.appcompat:appcompat:1.7.0 (*)
++--- com.mikepenz:iconics-core:5.5.0-b01 (*) ++--- com.mikepenz:iconics-core:5.5.0-b01 (*)
++--- io.noties.markwon:core:4.6.2 ++--- io.noties.markwon:core:4.6.2

@ -90,7 +90,6 @@ androidx-preference = { module = "androidx.preference:preference", version.ref =
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", 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-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" }

@ -1,6 +1,5 @@
package org.tasks.data package org.tasks.data
import co.touchlab.kermit.Logger
import org.tasks.data.TaskListQueryNonRecursive.getNonRecursiveQuery import org.tasks.data.TaskListQueryNonRecursive.getNonRecursiveQuery
import org.tasks.data.TaskListQueryRecursive.getRecursiveQuery import org.tasks.data.TaskListQueryRecursive.getRecursiveQuery
import org.tasks.data.entity.CaldavAccount 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.CaldavTask
import org.tasks.data.entity.Geofence import org.tasks.data.entity.Geofence
import org.tasks.data.entity.Place import org.tasks.data.entity.Place
import org.tasks.data.entity.Tag
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.data.sql.Criterion import org.tasks.data.sql.Criterion
import org.tasks.data.sql.Field.Companion.field 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.AstridOrderingFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.preferences.QueryPreferences import org.tasks.preferences.QueryPreferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
object TaskListQuery { object TaskListQuery {
private const val CALDAV_METADATA_JOIN = "for_caldav" private const val CALDAV_METADATA_JOIN = "for_caldav"
@ -24,9 +21,11 @@ object TaskListQuery {
Task.ID.eq(field("$CALDAV_METADATA_JOIN.cd_task")), Task.ID.eq(field("$CALDAV_METADATA_JOIN.cd_task")),
field("$CALDAV_METADATA_JOIN.cd_deleted").eq(0)) field("$CALDAV_METADATA_JOIN.cd_deleted").eq(0))
val JOINS = """ val JOINS = """
${Join.left(Tag.TABLE, Tag.TASK.eq(Task.ID))}
${Join.left(CaldavTask.TABLE.`as`(CALDAV_METADATA_JOIN), JOIN_CALDAV)} ${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(CaldavAccount.TABLE, CaldavCalendar.ACCOUNT.eq(CaldavAccount.UUID))}
${Join.left(Geofence.TABLE, Geofence.TASK.eq(Task.ID))} ${Join.left(Geofence.TABLE, Geofence.TASK.eq(Task.ID))}
${Join.left(Place.TABLE, Place.UID.eq(Geofence.PLACE))} ${Join.left(Place.TABLE, Place.UID.eq(Geofence.PLACE))}
@ -42,19 +41,13 @@ object TaskListQuery {
fun getQuery( fun getQuery(
preferences: QueryPreferences, preferences: QueryPreferences,
filter: Filter, filter: Filter,
): String { ): MutableList<String> = when {
val start = currentTimeMillis() filter.supportsManualSort() && preferences.isManualSort ->
return when { getRecursiveQuery(filter, preferences)
filter.supportsManualSort() && preferences.isManualSort -> filter is AstridOrderingFilter && preferences.isAstridSort ->
getRecursiveQuery(filter, preferences) getNonRecursiveQuery(filter, preferences)
filter.supportsSorting() ->
filter is AstridOrderingFilter && preferences.isAstridSort -> getRecursiveQuery(filter, preferences)
getNonRecursiveQuery(filter, preferences) else -> getNonRecursiveQuery(filter, preferences)
filter.supportsSorting() ->
getRecursiveQuery(filter, preferences)
else -> getNonRecursiveQuery(filter, preferences)
}.also { Logger.v { "Building query took ${currentTimeMillis() - start}ms" } }
} }
} }

@ -29,7 +29,7 @@ internal object TaskListQueryNonRecursive {
field("tasks.completed > 0").`as`("parentComplete") field("tasks.completed > 0").`as`("parentComplete")
)).toTypedArray() )).toTypedArray()
fun getNonRecursiveQuery(filter: Filter, preferences: QueryPreferences): String { fun getNonRecursiveQuery(filter: Filter, preferences: QueryPreferences): MutableList<String> {
val joinedQuery = JOINS + if (filter is AstridOrderingFilter) filter.getSqlQuery() else filter.sql!! val joinedQuery = JOINS + if (filter is AstridOrderingFilter) filter.getSqlQuery() else filter.sql!!
val sortMode = preferences.sortMode val sortMode = preferences.sortMode
val groupMode = preferences.groupMode val groupMode = preferences.groupMode
@ -52,9 +52,10 @@ internal object TaskListQueryNonRecursive {
else -> else ->
"$query GROUP BY ${Task.ID}" "$query GROUP BY ${Task.ID}"
} }
return Query.select(*FIELDS.plus(sortGroup)) return mutableListOf(
.withQueryTemplate(PermaSql.replacePlaceholdersForQuery(groupedQuery)) Query.select(*FIELDS.plus(sortGroup))
.from(Task.TABLE) .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(groupedQuery))
.toString() .from(Task.TABLE)
.toString())
} }
} }

@ -5,10 +5,12 @@ import com.todoroo.astrid.core.SortHelper
import org.tasks.data.dao.TaskDao.TaskCriteria.activeAndVisible import org.tasks.data.dao.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.data.db.Table import org.tasks.data.db.Table
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Tag
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.data.sql.Criterion import org.tasks.data.sql.Criterion
import org.tasks.data.sql.Field.Companion.field import org.tasks.data.sql.Field.Companion.field
import org.tasks.data.sql.Join import org.tasks.data.sql.Join
import org.tasks.data.sql.Query
import org.tasks.data.sql.QueryTemplate import org.tasks.data.sql.QueryTemplate
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
@ -17,17 +19,32 @@ import org.tasks.preferences.QueryPreferences
internal object TaskListQueryRecursive { internal object TaskListQueryRecursive {
private val RECURSIVE = Table("recursive_tasks") private val RECURSIVE = Table("recursive_tasks")
private val RECURSIVE_TASK = field("$RECURSIVE.task") 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 = private val SUBTASK_QUERY =
QueryTemplate() QueryTemplate()
.join(Join.inner(RECURSIVE, Task.PARENT.eq(RECURSIVE_TASK))) .join(Join.inner(RECURSIVE, Task.PARENT.eq(RECURSIVE_TASK)))
.where(activeAndVisible()) .where(activeAndVisible())
.toString()
// TODO: switch to datastore, reading from preferences is expensive (30+ ms)
fun getRecursiveQuery( fun getRecursiveQuery(
filter: Filter, filter: Filter,
preferences: QueryPreferences, preferences: QueryPreferences,
): String { ): MutableList<String> {
val parentQuery = when (filter) { val parentQuery = when (filter) {
is CaldavFilter -> newCaldavQuery(filter.uuid) is CaldavFilter -> newCaldavQuery(filter.uuid)
else -> PermaSql.replacePlaceholdersForQuery(filter.sql!!) else -> PermaSql.replacePlaceholdersForQuery(filter.sql!!)
@ -57,103 +74,48 @@ internal object TaskListQueryRecursive {
preferences.sortAscending && sortMode != SortHelper.SORT_GTASKS && sortMode != SortHelper.SORT_CALDAV preferences.sortAscending && sortMode != SortHelper.SORT_GTASKS && sortMode != SortHelper.SORT_CALDAV
val subtaskAscending = val subtaskAscending =
preferences.subtaskAscending && subtaskMode != SortHelper.SORT_GTASKS && subtaskMode != SortHelper.SORT_CALDAV preferences.subtaskAscending && subtaskMode != SortHelper.SORT_GTASKS && subtaskMode != SortHelper.SORT_CALDAV
val completedAtBottom = preferences.completedTasksAtBottom val primaryGroupSelector = SortHelper.orderSelectForSortTypeRecursive(groupMode, true)
val parentCompleted = if (completedAtBottom) "tasks.completed > 0" else "0" val primarySortSelect = SortHelper.orderSelectForSortTypeRecursive(sortMode, false)
val completionSort = if (completedAtBottom) { 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)" "(CASE WHEN tasks.completed > 0 THEN ${SortHelper.orderSelectForSortTypeRecursive(completedMode, false)} ELSE 0 END)"
} else { } else {
"0" "0"
} }
val query = """ val withClause = """
WITH RECURSIVE recursive_tasks AS ( CREATE TEMPORARY TABLE `recursive_tasks` AS
SELECT 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 (
tasks._id AS task, 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)}
$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 FROM tasks
${ ${
if (groupMode == SortHelper.SORT_LIST) { if (groupMode == SortHelper.SORT_LIST) {
""" """
INNER JOIN caldav_tasks ON cd_task = tasks._id AND cd_deleted = 0 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_lists on cd_calendar = cdl_uuid
""".trimIndent() """.trimIndent()
} else { } else {
"" ""
} }
} }
$parentQuery $parentQuery
UNION ALL SELECT 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
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 $SUBTASK_QUERY
ORDER BY 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)}
parent_complete, ) SELECT * FROM recursive_tasks
indent DESC, WHERE indent = (SELECT MAX(indent) FROM recursive_tasks as r WHERE r.task = recursive_tasks.task)
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
""".trimIndent() """.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) = private fun newCaldavQuery(list: String) =

Loading…
Cancel
Save