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.viewmodel)
implementation(libs.androidx.room)
implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat)
implementation(libs.iconics)
implementation(libs.markwon)

@ -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)
}

@ -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)
}

@ -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) })
}
}

@ -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<in Task?, *>): Long {
val task = newTask(*properties)

@ -105,7 +105,7 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
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)
)
}
}

@ -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()
}

@ -144,7 +144,8 @@ class TaskDao @Inject constructor(
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()

@ -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<TaskContainer> =
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<Task> = fetchFiltered(fi
suspend fun TaskDao.fetchFiltered(queryTemplate: String): List<Task> {
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 =

@ -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()

@ -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 {

@ -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<String> {
subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences)
return getQuery(widgetPreferences, filter)
}

@ -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<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? =
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.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,
){

@ -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<TaskContainer> {
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<TaskContainer>
open suspend fun fetchTasks(callback: suspend () -> List<String>): List<TaskContainer> =
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<TaskContainer> =
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<Long>, now: Long = DateTimeUtils2.currentTimeMillis()) =
ids.eachChunk { internalTouch(it, now) }

@ -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

@ -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

@ -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" }

@ -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<String> = 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)
}
}

@ -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<String> {
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())
}
}

@ -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<String> {
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) =

Loading…
Cancel
Save