From c81bb0238ee0af02bff4a8c52f97c7c0a644a7d6 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 26 Jan 2025 19:11:19 +0100 Subject: [PATCH] refactor: Reformat SQL queries for ease of reading (#1237) The database queries in the @Query annotations were in a range of different styles which made them difficult to read, and difficult to write new ones in a consistent style. Fix this. Write a new tool, `sqlfmt`. This processes the DAO files looking for `@Query(...)` annotations. It extracts the SQL from those annotations and calls `sqlfluff` (https://github.com/sqlfluff/sqlfluff, which must be installed separately) to lint and fix formatting issues in the SQL. The file is re-written with the newly formatted SQL queries. --- .editorconfig | 2 +- .sqlfluff | 32 ++ .../pachli/core/database/dao/AccountDao.kt | 351 ++++++------ .../core/database/dao/AnnouncementsDao.kt | 24 +- .../core/database/dao/ContentFiltersDao.kt | 16 +- .../core/database/dao/ConversationsDao.kt | 60 +- .../app/pachli/core/database/dao/DraftDao.kt | 50 +- .../core/database/dao/FollowingAccountDao.kt | 16 +- .../pachli/core/database/dao/InstanceDao.kt | 24 +- .../app/pachli/core/database/dao/ListsDao.kt | 39 +- .../pachli/core/database/dao/LogEntryDao.kt | 12 +- .../core/database/dao/NotificationDao.kt | 343 +++++++----- .../pachli/core/database/dao/RemoteKeyDao.kt | 50 +- .../pachli/core/database/dao/TimelineDao.kt | 520 ++++++++++++------ .../core/database/dao/TranslatedStatusDao.kt | 11 +- gradle/libs.versions.toml | 1 + settings.gradle.kts | 1 + tools/fmtsql/README.md | 30 + tools/fmtsql/build.gradle.kts | 34 ++ .../src/main/kotlin/app/pachli/fmtsql/Main.kt | 219 ++++++++ 20 files changed, 1290 insertions(+), 545 deletions(-) create mode 100644 .sqlfluff create mode 100644 tools/fmtsql/README.md create mode 100644 tools/fmtsql/build.gradle.kts create mode 100644 tools/fmtsql/src/main/kotlin/app/pachli/fmtsql/Main.kt diff --git a/.editorconfig b/.editorconfig index 974beb73a4..2e07789832 100644 --- a/.editorconfig +++ b/.editorconfig @@ -40,5 +40,5 @@ indent_size = 2 # Disable ktlint on generated source code, see # https://github.com/JLLeitschuh/ktlint-gradle/issues/746 -[**/build/generated/source/**] +[**/build/generated/**] ktlint = disabled diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 0000000000..919ee3e044 --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,32 @@ +[sqlfluff] +dialect = sqlite +templater = placeholder +max_line_length = 120 + +# Disable specific rules: +# - CP02 so identifier case is left alone. +# - AM04, "Query produces an unknown number of result columns", allow "SELECT *" +# - ST10, bug with placeholders in 3.3.0, https://github.com/sqlfluff/sqlfluff/issues/6493 +exclude_rules = CP02,AM04,ST10 + +[sqlfluff:templater:placeholder] +param_style = colon + +# Force a line break before FROM. +[sqlfluff:layout:type:from_clause] +keyword_line_position = leading + +# Force `SET` to a separate line. +[sqlfluff:layout:type:set_clause_list] +keyword_line_position = alone + +[sqlfluff:rules:capitalisation.keywords] +capitalisation_policy = upper +[sqlfluff:rules:capitalisation.identifiers] +#extended_capitalisation_policy = camel +[sqlfluff:rules:capitalisation.functions] +extended_capitalisation_policy = upper +[sqlfluff:rules:capitalisation.literals] +capitalisation_policy = upper +[sqlfluff:rules:capitalisation.types] +extended_capitalisation_policy = upper diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/AccountDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/AccountDao.kt index 5c147f8d9f..1bc8f73c4f 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/AccountDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/AccountDao.kt @@ -38,39 +38,39 @@ interface AccountDao { @Transaction @Query( """ - SELECT * - FROM AccountEntity - WHERE id = :accountId - """, +SELECT * +FROM AccountEntity +WHERE id = :accountId +""", ) suspend fun getPachliAccount(accountId: Long): PachliAccount? @Transaction @Query( """ - SELECT * - FROM AccountEntity - WHERE id = :accountId - """, +SELECT * +FROM AccountEntity +WHERE id = :accountId +""", ) fun getPachliAccountFlow(accountId: Long): Flow @Transaction @Query( """ - SELECT * - FROM AccountEntity - WHERE isActive = 1 - """, +SELECT * +FROM AccountEntity +WHERE isActive = 1 +""", ) fun getActivePachliAccountFlow(): Flow @Transaction @Query( """ - SELECT * - FROM AccountEntity - """, +SELECT * +FROM AccountEntity +""", ) fun loadAllPachliAccountFlow(): Flow> @@ -89,87 +89,107 @@ interface AccountDao { @Delete suspend fun delete(account: AccountEntity) - @Query("SELECT * FROM AccountEntity ORDER BY id ASC") + @Query( + """ +SELECT * +FROM AccountEntity +ORDER BY id ASC +""", + ) fun loadAllFlow(): Flow> - @Query("SELECT * FROM AccountEntity ORDER BY id ASC") + @Query( + """ +SELECT * +FROM AccountEntity +ORDER BY id ASC +""", + ) suspend fun loadAll(): List - @Query("SELECT id FROM AccountEntity WHERE isActive = 1") + @Query( + """ +SELECT id +FROM AccountEntity +WHERE isActive = 1 +""", + ) fun getActiveAccountId(): Flow @Query( """ - SELECT * - FROM AccountEntity - ORDER BY isActive DESC, id ASC - """, +SELECT * +FROM AccountEntity +ORDER BY isActive DESC, id ASC +""", ) fun getAccountsOrderedByActive(): Flow> @Query( """ - SELECT * - FROM AccountEntity - WHERE isActive = 1 - """, +SELECT * +FROM AccountEntity +WHERE isActive = 1 +""", ) fun getActiveAccountFlow(): Flow @Query( """ - SELECT * - FROM AccountEntity - WHERE isActive = 1 - """, +SELECT * +FROM AccountEntity +WHERE isActive = 1 +""", ) suspend fun getActiveAccount(): AccountEntity? @Query( """ - UPDATE AccountEntity - SET isActive = 0 - """, +UPDATE AccountEntity +SET + isActive = 0 +""", ) suspend fun clearActiveAccount() @Query( """ - SELECT * - FROM AccountEntity - WHERE id = :id - """, +SELECT * +FROM AccountEntity +WHERE id = :id +""", ) suspend fun getAccountById(id: Long): AccountEntity? @Query( """ - SELECT * - FROM AccountEntity - WHERE domain = :domain AND accountId = :accountId - """, +SELECT * +FROM AccountEntity +WHERE domain = :domain AND accountId = :accountId +""", ) suspend fun getAccountByIdAndDomain(accountId: String, domain: String): AccountEntity? @Query( """ - SELECT COUNT(id) - FROM AccountEntity - WHERE notificationsEnabled = 1 - """, +SELECT COUNT(id) +FROM AccountEntity +WHERE notificationsEnabled = 1 +""", ) suspend fun countAccountsWithNotificationsEnabled(): Int @Query( """ - UPDATE AccountEntity - SET unifiedPushUrl = :unifiedPushUrl, - pushServerKey = :pushServerKey, - pushAuth = :pushAuth, - pushPrivKey = :pushPrivKey, - pushPubKey = :pushPubKey - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + unifiedPushUrl = :unifiedPushUrl, + pushServerKey = :pushServerKey, + pushAuth = :pushAuth, + pushPrivKey = :pushPrivKey, + pushPubKey = :pushPubKey +WHERE id = :accountId +""", ) suspend fun setPushNotificationData( accountId: Long, @@ -182,242 +202,267 @@ interface AccountDao { @Query( """ - UPDATE AccountEntity - SET alwaysShowSensitiveMedia = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + alwaysShowSensitiveMedia = :value +WHERE id = :accountId +""", ) suspend fun setAlwaysShowSensitiveMedia(accountId: Long, value: Boolean) @Query( """ - UPDATE AccountEntity - SET alwaysOpenSpoiler = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + alwaysOpenSpoiler = :value +WHERE id = :accountId +""", ) suspend fun setAlwaysOpenSpoiler(accountId: Long, value: Boolean) @Query( """ - UPDATE AccountEntity - SET mediaPreviewEnabled = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + mediaPreviewEnabled = :value +WHERE id = :accountId +""", ) suspend fun setMediaPreviewEnabled(accountId: Long, value: Boolean) @Query( """ - UPDATE AccountEntity - SET tabPreferences = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + tabPreferences = :value +WHERE id = :accountId +""", ) suspend fun setTabPreferences(accountId: Long, value: List) @Query( """ - UPDATE AccountEntity - SET notificationMarkerId = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationMarkerId = :value +WHERE id = :accountId +""", ) suspend fun setNotificationMarkerId(accountId: Long, value: String) @Query( """ - UPDATE AccountEntity - SET notificationsFilter = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsFilter = :value +WHERE id = :accountId +""", ) suspend fun setNotificationsFilter(accountId: Long, value: String) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET defaultPostPrivacy = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + defaultPostPrivacy = :value +WHERE id = :accountId +""", ) fun setDefaultPostPrivacy(accountId: Long, value: Status.Visibility) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET defaultMediaSensitivity = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + defaultMediaSensitivity = :value +WHERE id = :accountId +""", ) fun setDefaultMediaSensitivity(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET defaultPostLanguage = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + defaultPostLanguage = :value +WHERE id = :accountId +""", ) fun setDefaultPostLanguage(accountId: Long, value: String) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsEnabled = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsEnabled = :value +WHERE id = :accountId +""", ) fun setNotificationsEnabled(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsFollowed = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsFollowed = :value +WHERE id = :accountId +""", ) fun setNotificationsFollowed(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsFollowRequested = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsFollowRequested = :value +WHERE id = :accountId +""", ) fun setNotificationsFollowRequested(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsReblogged = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsReblogged = :value +WHERE id = :accountId +""", ) fun setNotificationsReblogged(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsFavorited = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsFavorited = :value +WHERE id = :accountId +""", ) fun setNotificationsFavorited(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsPolls = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsPolls = :value +WHERE id = :accountId +""", ) fun setNotificationsPolls(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsSubscriptions = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsSubscriptions = :value +WHERE id = :accountId +""", ) fun setNotificationsSubscriptions(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsSignUps = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsSignUps = :value +WHERE id = :accountId +""", ) fun setNotificationsSignUps(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsUpdates = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsUpdates = :value +WHERE id = :accountId +""", ) fun setNotificationsUpdates(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationsReports = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationsReports = :value +WHERE id = :accountId +""", ) fun setNotificationsReports(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationSound = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationSound = :value +WHERE id = :accountId +""", ) fun setNotificationSound(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationVibration = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationVibration = :value +WHERE id = :accountId +""", ) fun setNotificationVibration(accountId: Long, value: Boolean) // TODO: Should be suspend @Query( """ - UPDATE AccountEntity - SET notificationLight = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationLight = :value +WHERE id = :accountId +""", ) fun setNotificationLight(accountId: Long, value: Boolean) @Query( """ - UPDATE AccountEntity - SET notificationAccountFilterNotFollowed = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationAccountFilterNotFollowed = :value +WHERE id = :accountId +""", ) suspend fun setNotificationAccountFilterNotFollowed(accountId: Long, value: FilterAction) @Query( """ - UPDATE AccountEntity - SET notificationAccountFilterYounger30d = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationAccountFilterYounger30d = :value +WHERE id = :accountId +""", ) suspend fun setNotificationAccountFilterYounger30d(accountId: Long, value: FilterAction) @Query( """ - UPDATE AccountEntity - SET notificationAccountFilterLimitedByServer = :value - WHERE id = :accountId - """, +UPDATE AccountEntity +SET + notificationAccountFilterLimitedByServer = :value +WHERE id = :accountId +""", ) suspend fun setNotificationAccountFilterLimitedByServer(accountId: Long, value: FilterAction) } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/AnnouncementsDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/AnnouncementsDao.kt index 023f95dba5..75be8c7c50 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/AnnouncementsDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/AnnouncementsDao.kt @@ -29,10 +29,10 @@ import app.pachli.core.database.model.AnnouncementEntity interface AnnouncementsDao { @Query( """ - DELETE - FROM AnnouncementEntity - WHERE accountId = :accountId - """, +DELETE +FROM AnnouncementEntity +WHERE accountId = :accountId +""", ) suspend fun deleteAllForAccount(accountId: Long) @@ -44,19 +44,19 @@ interface AnnouncementsDao { @Query( """ - DELETE - FROM AnnouncementEntity - WHERE accountId = :pachliAccountId AND announcementId = :announcementId - """, +DELETE +FROM AnnouncementEntity +WHERE accountId = :pachliAccountId AND announcementId = :announcementId +""", ) suspend fun deleteForAccount(pachliAccountId: Long, announcementId: String) @Query( """ - SELECT * - FROM AnnouncementEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM AnnouncementEntity +WHERE accountId = :pachliAccountId +""", ) suspend fun loadAllForAccount(pachliAccountId: Long): List } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/ContentFiltersDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/ContentFiltersDao.kt index 9312e1f53b..af6c5c5a21 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/ContentFiltersDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/ContentFiltersDao.kt @@ -30,19 +30,19 @@ import kotlinx.coroutines.flow.Flow interface ContentFiltersDao { @Query( """ - SELECT * - FROM ContentFiltersEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM ContentFiltersEntity +WHERE accountId = :pachliAccountId +""", ) suspend fun getByAccount(pachliAccountId: Long): ContentFiltersEntity? @Query( """ - SELECT * - FROM ContentFiltersEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM ContentFiltersEntity +WHERE accountId = :pachliAccountId +""", ) fun flowByAccount(pachliAccountId: Long): Flow diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/ConversationsDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/ConversationsDao.kt index 5e02f49057..dc22d7f552 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/ConversationsDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/ConversationsDao.kt @@ -32,29 +32,49 @@ interface ConversationsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(conversation: ConversationEntity) - @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") + @Query( + """ +DELETE +FROM ConversationEntity +WHERE id = :id AND accountId = :accountId +""", + ) suspend fun delete(id: String, accountId: Long) - @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY `order` ASC") + @Query( + """ +SELECT * +FROM ConversationEntity +WHERE accountId = :accountId +ORDER BY `order` ASC +""", + ) fun conversationsForAccount(accountId: Long): PagingSource @Deprecated("Use conversationsForAccount, this is only for use in tests") @Query( """ - SELECT * - FROM ConversationEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM ConversationEntity +WHERE accountId = :pachliAccountId +""", ) suspend fun loadAllForAccount(pachliAccountId: Long): List - @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") + @Query( + """ +DELETE +FROM ConversationEntity +WHERE accountId = :accountId +""", + ) suspend fun deleteForAccount(accountId: Long) @Query( """ UPDATE ConversationEntity -SET s_bookmarked = :bookmarked +SET + s_bookmarked = :bookmarked WHERE accountId = :accountId AND s_id = :lastStatusId """, ) @@ -63,7 +83,8 @@ WHERE accountId = :accountId AND s_id = :lastStatusId @Query( """ UPDATE ConversationEntity -SET s_collapsed = :collapsed +SET + s_collapsed = :collapsed WHERE accountId = :accountId AND s_id = :lastStatusId """, ) @@ -72,7 +93,8 @@ WHERE accountId = :accountId AND s_id = :lastStatusId @Query( """ UPDATE ConversationEntity -SET s_expanded = :expanded +SET + s_expanded = :expanded WHERE accountId = :accountId AND s_id = :lastStatusId """, ) @@ -81,7 +103,8 @@ WHERE accountId = :accountId AND s_id = :lastStatusId @Query( """ UPDATE ConversationEntity -SET s_favourited = :favourited +SET + s_favourited = :favourited WHERE accountId = :accountId AND s_id = :lastStatusId """, ) @@ -90,7 +113,8 @@ WHERE accountId = :accountId AND s_id = :lastStatusId @Query( """ UPDATE ConversationEntity -SET s_muted = :muted +SET + s_muted = :muted WHERE accountId = :accountId AND s_id = :lastStatusId """, ) @@ -99,7 +123,8 @@ WHERE accountId = :accountId AND s_id = :lastStatusId @Query( """ UPDATE ConversationEntity -SET s_showingHiddenContent = :showingHiddenContent +SET + s_showingHiddenContent = :showingHiddenContent WHERE accountId = :accountId AND s_id = :lastStatusId """, ) @@ -107,10 +132,11 @@ WHERE accountId = :accountId AND s_id = :lastStatusId @Query( """ - UPDATE ConversationEntity - SET s_poll = :poll - WHERE accountId = :accountId AND s_id = :lastStatusId - """, +UPDATE ConversationEntity +SET + s_poll = :poll +WHERE accountId = :accountId AND s_id = :lastStatusId +""", ) suspend fun setVoted(accountId: Long, lastStatusId: String, poll: String) } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/DraftDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/DraftDao.kt index 8058b4c9dc..8580f59b7c 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/DraftDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/DraftDao.kt @@ -31,21 +31,59 @@ interface DraftDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplace(draft: DraftEntity) - @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") + @Query( + """ +SELECT * +FROM DraftEntity +WHERE accountId = :accountId +ORDER BY id ASC +""", + ) fun draftsPagingSource(accountId: Long): PagingSource - @Query("SELECT COUNT(*) FROM DraftEntity WHERE accountId = :accountId AND failedToSendNew = 1") + @Query( + """ +SELECT COUNT(*) +FROM DraftEntity +WHERE accountId = :accountId AND failedToSendNew = 1 +""", + ) fun draftsNeedUserAlert(accountId: Long): LiveData - @Query("UPDATE DraftEntity SET failedToSendNew = 0 WHERE accountId = :accountId AND failedToSendNew = 1") + @Query( + """ +UPDATE DraftEntity +SET + failedToSendNew = 0 +WHERE accountId = :accountId AND failedToSendNew = 1 +""", + ) suspend fun draftsClearNeedUserAlert(accountId: Long) - @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId") + @Query( + """ +SELECT * +FROM DraftEntity +WHERE accountId = :accountId +""", + ) suspend fun loadDrafts(accountId: Long): List - @Query("DELETE FROM DraftEntity WHERE id = :id") + @Query( + """ +DELETE +FROM DraftEntity +WHERE id = :id +""", + ) suspend fun delete(id: Int) - @Query("SELECT * FROM DraftEntity WHERE id = :id") + @Query( + """ +SELECT * +FROM DraftEntity +WHERE id = :id +""", + ) suspend fun find(id: Int): DraftEntity? } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/FollowingAccountDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/FollowingAccountDao.kt index 0d35d14190..534cec08ad 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/FollowingAccountDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/FollowingAccountDao.kt @@ -30,10 +30,10 @@ import app.pachli.core.database.model.FollowingAccountEntity interface FollowingAccountDao { @Query( """ - DELETE - FROM FollowingAccountEntity - WHERE pachliAccountId = :accountId - """, +DELETE +FROM FollowingAccountEntity +WHERE pachliAccountId = :accountId +""", ) suspend fun deleteAllForAccount(accountId: Long) @@ -48,10 +48,10 @@ interface FollowingAccountDao { @Query( """ - SELECT * - FROM FollowingAccountEntity - WHERE pachliAccountId = :pachliAccountId - """, +SELECT * +FROM FollowingAccountEntity +WHERE pachliAccountId = :pachliAccountId +""", ) suspend fun loadAllForAccount(pachliAccountId: Long): List } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/InstanceDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/InstanceDao.kt index 36c7bcb919..5bf954eb51 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/InstanceDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/InstanceDao.kt @@ -39,13 +39,31 @@ interface InstanceDao { suspend fun upsert(serverEntity: ServerEntity) @Transaction - @Query("SELECT * FROM InstanceInfoEntity WHERE instance = :instance LIMIT 1") + @Query( + """ +SELECT * +FROM InstanceInfoEntity +WHERE instance = :instance LIMIT 1 +""", + ) suspend fun getInstanceInfo(instance: String): InstanceInfoEntity? - @Query("SELECT * FROM ServerEntity WHERE accountId = :pachliAccountId") + @Query( + """ +SELECT * +FROM ServerEntity +WHERE accountId = :pachliAccountId +""", + ) suspend fun getServer(pachliAccountId: Long): ServerEntity? @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM EmojisEntity WHERE accountId = :pachliAccountId") + @Query( + """ +SELECT * +FROM EmojisEntity +WHERE accountId = :pachliAccountId +""", + ) suspend fun getEmojiInfo(pachliAccountId: Long): EmojisEntity? } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/ListsDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/ListsDao.kt index 75172cd23d..4dc1131660 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/ListsDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/ListsDao.kt @@ -30,32 +30,37 @@ import kotlinx.coroutines.flow.Flow interface ListsDao { @Query( """ - DELETE - FROM MastodonListEntity - WHERE accountId = :pachliAccountId - """, +DELETE +FROM MastodonListEntity +WHERE accountId = :pachliAccountId +""", ) suspend fun deleteAllForAccount(pachliAccountId: Long) @Query( """ - SELECT * - FROM MastodonListEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM MastodonListEntity +WHERE accountId = :pachliAccountId +""", ) fun flowByAccount(pachliAccountId: Long): Flow> @Query( """ - SELECT * - FROM MastodonListEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM MastodonListEntity +WHERE accountId = :pachliAccountId +""", ) suspend fun get(pachliAccountId: Long): List - @Query("SELECT * FROM MastodonListEntity") + @Query( + """ +SELECT * +FROM MastodonListEntity +""", + ) fun flowAll(): Flow> @Upsert @@ -66,10 +71,10 @@ interface ListsDao { @Query( """ - DELETE - FROM MastodonListEntity - WHERE accountId = :pachliAccountId AND listId = :listId - """, +DELETE +FROM MastodonListEntity +WHERE accountId = :pachliAccountId AND listId = :listId +""", ) suspend fun deleteForAccount(pachliAccountId: Long, listId: String) } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/LogEntryDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/LogEntryDao.kt index 0d0fff85ad..02cddf5500 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/LogEntryDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/LogEntryDao.kt @@ -37,9 +37,9 @@ interface LogEntryDao { @Query( """ SELECT * - FROM LogEntryEntity - ORDER BY id ASC - """, +FROM LogEntryEntity +ORDER BY id ASC +""", ) suspend fun loadAll(): List @@ -48,9 +48,9 @@ SELECT * @Query( """ DELETE - FROM LogEntryEntity - WHERE instant < :cutoff - """, +FROM LogEntryEntity +WHERE instant < :cutoff +""", ) suspend fun prune(cutoff: Instant) } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/NotificationDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/NotificationDao.kt index 016b97bf44..de108778af 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/NotificationDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/NotificationDao.kt @@ -39,116 +39,169 @@ interface NotificationDao { @Transaction @Query( """ - SELECT +SELECT -- Basic notification info -n.pachliAccountId, -n.serverId, -n.type, -n.createdAt, -n.accountServerId, -n.statusServerId, - --- The account that triggered the notification -a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', -a.localUsername as 'a_localUsername', a.username as 'a_username', -a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', -a.emojis as 'a_emojis', a.bot as 'a_bot', a.createdAt as 'a_createdAt', a.limited as 'a_limited', -a.note as 'a_note', - --- The status in the notification (if any) -s.serverId as 's_serverId', s.url as 's_url', s.timelineUserId as 's_timelineUserId', -s.authorServerId as 's_authorServerId', s.inReplyToId as 's_inReplyToId', -s.inReplyToAccountId as 's_inReplyToAccountId', s.createdAt as 's_createdAt', -s.editedAt as 's_editedAt', -s.emojis as 's_emojis', s.reblogsCount as 's_reblogsCount', -s.favouritesCount as 's_favouritesCount', s.repliesCount as 's_repliesCount', -s.reblogged as 's_reblogged', s.favourited as 's_favourited', -s.bookmarked as 's_bookmarked', s.sensitive as 's_sensitive', -s.spoilerText as 's_spoilerText', s.visibility as 's_visibility', -s.mentions as 's_mentions', s.tags as 's_tags', s.application as 's_application', -s.reblogServerId as 's_reblogServerId',s.reblogAccountId as 's_reblogAccountId', -s.content as 's_content', s.attachments as 's_attachments', s.poll as 's_poll', -s.card as 's_card', s.muted as 's_muted', s.pinned as 's_pinned', s.language as 's_language', -s.filtered as 's_filtered', - --- The status' account (if any) -sa.serverId as 's_a_serverId', sa.timelineUserId as 's_a_timelineUserId', -sa.localUsername as 's_a_localUsername', sa.username as 's_a_username', -sa.displayName as 's_a_displayName', sa.url as 's_a_url', sa.avatar as 's_a_avatar', -sa.emojis as 's_a_emojis', sa.bot as 's_a_bot', sa.createdAt as 's_a_createdAt', sa.limited as 's_a_limited', -sa.note as 's_a_note', - --- The status's reblog account (if any) -rb.serverId as 's_rb_serverId', rb.timelineUserId 's_rb_timelineUserId', -rb.localUsername as 's_rb_localUsername', rb.username as 's_rb_username', -rb.displayName as 's_rb_displayName', rb.url as 's_rb_url', rb.avatar as 's_rb_avatar', -rb.emojis as 's_rb_emojis', rb.bot as 's_rb_bot', rb.createdAt as 's_rb_createdAt', rb.limited as 's_rb_limited', -rb.note as 's_rb_note', - --- Status view data -svd.serverId as 's_svd_serverId', svd.timelineUserId as 's_svd_timelineUserId', -svd.expanded as 's_svd_expanded', svd.contentShowing as 's_svd_contentShowing', -svd.contentCollapsed as 's_svd_contentCollapsed', svd.translationState as 's_svd_translationState', - --- Translation -t.serverId as 's_t_serverId', t.timelineUserId as 's_t_timelineUserId', t.content as 's_t_content', -t.spoilerText as 's_t_spoilerText', t.poll as 's_t_poll', t.attachments as 's_t_attachments', -t.provider as 's_t_provider', - --- NotificationViewData -nvd.pachliAccountId as 'nvd_pachliAccountId', -nvd.serverId as 'nvd_serverId', -nvd.contentFilterAction as 'nvd_contentFilterAction', -nvd.accountFilterDecision as 'nvd_accountFilterDecision', - --- NotificationReportEntity -report.pachliAccountId as 'report_pachliAccountId', -report.serverId as 'report_serverId', -report.actionTaken as 'report_actionTaken', -report.actionTakenAt as 'report_actionTakenAt', -report.category as 'report_category', -report.comment as 'report_comment', -report.forwarded as 'report_forwarded', -report.createdAt as 'report_createdAt', -report.statusIds as 'report_statusIds', -report.ruleIds as 'report_rulesIds', -report.target_serverId as 'report_target_serverId', -report.target_timelineUserId as 'report_target_timelineUserId', -report.target_localUsername as 'report_target_localUsername', -report.target_username as 'report_target_username', -report.target_displayName as 'report_target_displayName', -report.target_url as 'report_target_url', -report.target_avatar as 'report_target_avatar', -report.target_emojis as 'report_target_emojis', -report.target_bot as 'report_target_bot', -report.target_createdAt as 'report_target_createdAt', -report.target_limited as 'report_target_limited', -report.target_note as 'report_target_note', - --- NotificationRelationshipSeveranceEvent -rse.pachliAccountId as 'rse_pachliAccountId', -rse.serverId as 'rse_serverId', -rse.eventId as 'rse_eventId', -rse.type as 'rse_type', -rse.purged as 'rse_purged', -rse.followersCount as 'rse_followersCount', -rse.followingCount as 'rse_followingCount', -rse.createdAt as 'rse_createdAt' - -FROM NotificationEntity n -LEFT JOIN TimelineAccountEntity a ON (n.pachliAccountId = a.timelineUserId AND n.accountServerId = a.serverId) -LEFT JOIN TimelineStatusEntity s ON (n.pachliAccountId = s.timelineUserId AND n.statusServerId = s.serverId) -LEFT JOIN TimelineAccountEntity sa ON (n.pachliAccountId = sa.timelineUserId AND s.authorServerId = sa.serverId) -LEFT JOIN TimelineAccountEntity rb ON (n.pachliAccountId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) -LEFT JOIN StatusViewDataEntity svd ON (n.pachliAccountId = svd.timelineUserId AND (s.serverId = svd.serverId OR s.reblogServerId = svd.serverId)) -LEFT JOIN TranslatedStatusEntity t ON (n.pachliAccountId = t.timelineUserId AND (s.serverId = t.serverId OR s.reblogServerId = t.serverId)) -LEFT JOIN NotificationViewDataEntity nvd on (n.pachliAccountId = nvd.pachliAccountId AND n.serverId = nvd.serverId) -LEFT JOIN NotificationReportEntity report on (n.pachliAccountId = report.pachliAccountId AND n.serverId = report.serverId) -LEFT JOIN NotificationRelationshipSeveranceEventEntity rse on (n.pachliAccountId = rse.pachliAccountId AND n.serverId = rse.serverId) + n.pachliAccountId, + n.serverId, + n.type, + n.createdAt, + n.accountServerId, + n.statusServerId, + + -- The account that triggered the notification + a.serverId AS 'a_serverId', + a.timelineUserId AS 'a_timelineUserId', + a.localUsername AS 'a_localUsername', + a.username AS 'a_username', + a.displayName AS 'a_displayName', + a.url AS 'a_url', + a.avatar AS 'a_avatar', + a.emojis AS 'a_emojis', + a.bot AS 'a_bot', + a.createdAt AS 'a_createdAt', + a.limited AS 'a_limited', + a.note AS 'a_note', + + -- The status in the notification (if any) + s.serverId AS 's_serverId', + s.url AS 's_url', + s.timelineUserId AS 's_timelineUserId', + s.authorServerId AS 's_authorServerId', + s.inReplyToId AS 's_inReplyToId', + s.inReplyToAccountId AS 's_inReplyToAccountId', + s.createdAt AS 's_createdAt', + s.editedAt AS 's_editedAt', + s.emojis AS 's_emojis', + s.reblogsCount AS 's_reblogsCount', + s.favouritesCount AS 's_favouritesCount', + s.repliesCount AS 's_repliesCount', + s.reblogged AS 's_reblogged', + s.favourited AS 's_favourited', + s.bookmarked AS 's_bookmarked', + s.sensitive AS 's_sensitive', + s.spoilerText AS 's_spoilerText', + s.visibility AS 's_visibility', + s.mentions AS 's_mentions', + s.tags AS 's_tags', + s.application AS 's_application', + s.reblogServerId AS 's_reblogServerId', + s.reblogAccountId AS 's_reblogAccountId', + s.content AS 's_content', + s.attachments AS 's_attachments', + s.poll AS 's_poll', + s.card AS 's_card', + s.muted AS 's_muted', + s.pinned AS 's_pinned', + s.language AS 's_language', + s.filtered AS 's_filtered', + + -- The status' account (if any) + sa.serverId AS 's_a_serverId', + sa.timelineUserId AS 's_a_timelineUserId', + sa.localUsername AS 's_a_localUsername', + sa.username AS 's_a_username', + sa.displayName AS 's_a_displayName', + sa.url AS 's_a_url', + sa.avatar AS 's_a_avatar', + sa.emojis AS 's_a_emojis', + sa.bot AS 's_a_bot', + sa.createdAt AS 's_a_createdAt', + sa.limited AS 's_a_limited', + sa.note AS 's_a_note', + + -- The status's reblog account (if any) + rb.serverId AS 's_rb_serverId', + rb.timelineUserId AS 's_rb_timelineUserId', + rb.localUsername AS 's_rb_localUsername', + rb.username AS 's_rb_username', + rb.displayName AS 's_rb_displayName', + rb.url AS 's_rb_url', + rb.avatar AS 's_rb_avatar', + rb.emojis AS 's_rb_emojis', + rb.bot AS 's_rb_bot', + rb.createdAt AS 's_rb_createdAt', + rb.limited AS 's_rb_limited', + rb.note AS 's_rb_note', + + -- Status view data + svd.serverId AS 's_svd_serverId', + svd.timelineUserId AS 's_svd_timelineUserId', + svd.expanded AS 's_svd_expanded', + svd.contentShowing AS 's_svd_contentShowing', + svd.contentCollapsed AS 's_svd_contentCollapsed', + svd.translationState AS 's_svd_translationState', + + -- Translation + t.serverId AS 's_t_serverId', + t.timelineUserId AS 's_t_timelineUserId', + t.content AS 's_t_content', + t.spoilerText AS 's_t_spoilerText', + t.poll AS 's_t_poll', + t.attachments AS 's_t_attachments', + t.provider AS 's_t_provider', + + -- NotificationViewData + nvd.pachliAccountId AS 'nvd_pachliAccountId', + nvd.serverId AS 'nvd_serverId', + nvd.contentFilterAction AS 'nvd_contentFilterAction', + nvd.accountFilterDecision AS 'nvd_accountFilterDecision', + + -- NotificationReportEntity + report.pachliAccountId AS 'report_pachliAccountId', + report.serverId AS 'report_serverId', + report.actionTaken AS 'report_actionTaken', + report.actionTakenAt AS 'report_actionTakenAt', + report.category AS 'report_category', + report.comment AS 'report_comment', + report.forwarded AS 'report_forwarded', + report.createdAt AS 'report_createdAt', + report.statusIds AS 'report_statusIds', + report.ruleIds AS 'report_rulesIds', + report.target_serverId AS 'report_target_serverId', + report.target_timelineUserId AS 'report_target_timelineUserId', + report.target_localUsername AS 'report_target_localUsername', + report.target_username AS 'report_target_username', + report.target_displayName AS 'report_target_displayName', + report.target_url AS 'report_target_url', + report.target_avatar AS 'report_target_avatar', + report.target_emojis AS 'report_target_emojis', + report.target_bot AS 'report_target_bot', + report.target_createdAt AS 'report_target_createdAt', + report.target_limited AS 'report_target_limited', + report.target_note AS 'report_target_note', + + -- NotificationRelationshipSeveranceEvent + rse.pachliAccountId AS 'rse_pachliAccountId', + rse.serverId AS 'rse_serverId', + rse.eventId AS 'rse_eventId', + rse.type AS 'rse_type', + rse.purged AS 'rse_purged', + rse.followersCount AS 'rse_followersCount', + rse.followingCount AS 'rse_followingCount', + rse.createdAt AS 'rse_createdAt' + +FROM NotificationEntity AS n +LEFT JOIN TimelineAccountEntity AS a ON (n.pachliAccountId = a.timelineUserId AND n.accountServerId = a.serverId) +LEFT JOIN TimelineStatusEntity AS s ON (n.pachliAccountId = s.timelineUserId AND n.statusServerId = s.serverId) +LEFT JOIN TimelineAccountEntity AS sa ON (n.pachliAccountId = sa.timelineUserId AND s.authorServerId = sa.serverId) +LEFT JOIN TimelineAccountEntity AS rb ON (n.pachliAccountId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) +LEFT JOIN + StatusViewDataEntity AS svd + ON (n.pachliAccountId = svd.timelineUserId AND (s.serverId = svd.serverId OR s.reblogServerId = svd.serverId)) +LEFT JOIN + TranslatedStatusEntity AS t + ON (n.pachliAccountId = t.timelineUserId AND (s.serverId = t.serverId OR s.reblogServerId = t.serverId)) +LEFT JOIN NotificationViewDataEntity AS nvd ON (n.pachliAccountId = nvd.pachliAccountId AND n.serverId = nvd.serverId) +LEFT JOIN + NotificationReportEntity AS report + ON (n.pachliAccountId = report.pachliAccountId AND n.serverId = report.serverId) +LEFT JOIN + NotificationRelationshipSeveranceEventEntity AS rse + ON (n.pachliAccountId = rse.pachliAccountId AND n.serverId = rse.serverId) WHERE n.pachliAccountId = :pachliAccountId ORDER BY LENGTH(n.serverId) DESC, n.serverId DESC - """, +""", ) fun pagingSource(pachliAccountId: Long): PagingSource @@ -158,22 +211,35 @@ ORDER BY LENGTH(n.serverId) DESC, n.serverId DESC */ @Query( """ - SELECT rownum - FROM ( - SELECT t1.pachliAccountId AS pachliAccountId, t1.serverId, COUNT(t2.serverId) - 1 AS rownum - FROM NotificationEntity t1 - JOIN NotificationEntity t2 ON t1.pachliAccountId = t2.pachliAccountId AND (LENGTH(t1.serverId) <= LENGTH(t2.serverId) AND t1.serverId <= t2.serverId) - WHERE t1.pachliAccountId = :pachliAccountId - GROUP BY t1.serverId - ORDER BY length(t1.serverId) DESC, t1.serverId DESC - ) - WHERE serverId = :notificationId - """, +SELECT rownum +FROM ( + SELECT + t1.pachliAccountId, + t1.serverId, + COUNT(t2.serverId) - 1 AS rownum + FROM NotificationEntity AS t1 + INNER JOIN + NotificationEntity AS t2 + ON + t1.pachliAccountId = t2.pachliAccountId + AND (LENGTH(t1.serverId) <= LENGTH(t2.serverId) AND t1.serverId <= t2.serverId) + WHERE t1.pachliAccountId = :pachliAccountId + GROUP BY t1.serverId + ORDER BY LENGTH(t1.serverId) DESC, t1.serverId DESC +) +WHERE serverId = :notificationId +""", ) suspend fun getNotificationRowNumber(pachliAccountId: Long, notificationId: String): Int /** Remove all cached notifications for [pachliAccountId]. */ - @Query("DELETE FROM NotificationEntity WHERE pachliAccountId = :pachliAccountId") + @Query( + """ +DELETE +FROM NotificationEntity +WHERE pachliAccountId = :pachliAccountId +""", + ) suspend fun deleteAllNotificationsForAccount(pachliAccountId: Long) @Upsert @@ -194,21 +260,22 @@ ORDER BY LENGTH(n.serverId) DESC, n.serverId DESC @Deprecated("Only present for use in tests") @Query( """ - SELECT * - FROM NotificationEntity - WHERE pachliAccountId = :pachliAccountId - """, +SELECT * +FROM NotificationEntity +WHERE pachliAccountId = :pachliAccountId +""", ) suspend fun loadAllForAccount(pachliAccountId: Long): List @Deprecated("Only present for use in tests") @Query( """ - SELECT * - FROM NotificationViewDataEntity - WHERE pachliAccountId = :pachliAccountId - AND serverId = :serverId - """, +SELECT * +FROM NotificationViewDataEntity +WHERE + pachliAccountId = :pachliAccountId + AND serverId = :serverId +""", ) suspend fun loadViewData(pachliAccountId: Long, serverId: String): NotificationViewDataEntity? @@ -218,22 +285,24 @@ ORDER BY LENGTH(n.serverId) DESC, n.serverId DESC @Deprecated("Only present for use in tests") @Query( """ - SELECT * - FROM NotificationReportEntity - WHERE pachliAccountId = :pachliAccountId - AND reportId = :reportId - """, +SELECT * +FROM NotificationReportEntity +WHERE + pachliAccountId = :pachliAccountId + AND reportId = :reportId +""", ) suspend fun loadReportById(pachliAccountId: Long, reportId: String): NotificationReportEntity? @Deprecated("Only present for use in tests") @Query( """ - SELECT * - FROM NotificationRelationshipSeveranceEventEntity - WHERE pachliAccountId = :pachliAccountId - AND eventId = :eventId - """, +SELECT * +FROM NotificationRelationshipSeveranceEventEntity +WHERE + pachliAccountId = :pachliAccountId + AND eventId = :eventId +""", ) suspend fun loadRelationshipSeveranceeventById(pachliAccountId: Long, eventId: String): NotificationRelationshipSeveranceEventEntity? } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/RemoteKeyDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/RemoteKeyDao.kt index 7b119ee043..2f9c313df7 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/RemoteKeyDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/RemoteKeyDao.kt @@ -27,42 +27,56 @@ interface RemoteKeyDao { @Upsert suspend fun upsert(remoteKey: RemoteKeyEntity) - @Query("SELECT * FROM RemoteKeyEntity WHERE accountId = :accountId AND timelineId = :timelineId AND kind = :kind") + @Query( + """ +SELECT * +FROM RemoteKeyEntity +WHERE accountId = :accountId AND timelineId = :timelineId AND kind = :kind +""", + ) suspend fun remoteKeyForKind(accountId: Long, timelineId: String, kind: RemoteKeyEntity.RemoteKeyKind): RemoteKeyEntity? - @Query("DELETE FROM RemoteKeyEntity WHERE accountId = :accountId AND timelineId = :timelineId") + @Query( + """ +DELETE +FROM RemoteKeyEntity +WHERE accountId = :accountId AND timelineId = :timelineId +""", + ) suspend fun delete(accountId: Long, timelineId: String) @Query( """ - DELETE - FROM RemoteKeyEntity - WHERE accountId = :accountId - AND timelineId = :timelineId - AND (kind = 'PREV' OR kind = 'NEXT') - """, +DELETE +FROM RemoteKeyEntity +WHERE + accountId = :accountId + AND timelineId = :timelineId + AND (kind = 'PREV' OR kind = 'NEXT') +""", ) suspend fun deletePrevNext(accountId: Long, timelineId: String) /** @return The remote key ID to use when refreshing. */ @Query( """ - SELECT `key` - FROM RemoteKeyEntity - WHERE accountId = :pachliAccountId - AND timelineId = :timelineId - AND kind = "REFRESH" - """, +SELECT `key` +FROM RemoteKeyEntity +WHERE + accountId = :pachliAccountId + AND timelineId = :timelineId + AND kind = 'REFRESH' +""", ) suspend fun getRefreshKey(pachliAccountId: Long, timelineId: String): String? /** @return All remote keys for [pachliAccountId]. */ @Query( """ - SELECT * - FROM RemoteKeyEntity - WHERE accountId = :pachliAccountId - """, +SELECT * +FROM RemoteKeyEntity +WHERE accountId = :pachliAccountId +""", ) suspend fun loadAllForAccount(pachliAccountId: Long): List } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/TimelineDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/TimelineDao.kt index a85751a96e..a1c5895cdf 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/TimelineDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/TimelineDao.kt @@ -51,34 +51,87 @@ abstract class TimelineDao { @Query( """ -SELECT s.serverId, s.url, s.timelineUserId, -s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt, -s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, -s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId, -s.content, s.attachments, s.poll, s.card, s.muted, s.pinned, s.language, s.filtered, -a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', -a.localUsername as 'a_localUsername', a.username as 'a_username', -a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', -a.emojis as 'a_emojis', a.bot as 'a_bot', a.createdAt as 'a_createdAt', a.limited as 'a_limited', -a.note as 'a_note', -rb.serverId as 'rb_serverId', rb.timelineUserId 'rb_timelineUserId', -rb.localUsername as 'rb_localUsername', rb.username as 'rb_username', -rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar', -rb.emojis as 'rb_emojis', rb.bot as 'rb_bot', rb.createdAt as 'rb_createdAt', rb.limited as 'rb_limited', -rb.note as 'rb_note', -svd.serverId as 'svd_serverId', svd.timelineUserId as 'svd_timelineUserId', -svd.expanded as 'svd_expanded', svd.contentShowing as 'svd_contentShowing', -svd.contentCollapsed as 'svd_contentCollapsed', svd.translationState as 'svd_translationState', -t.serverId as 't_serverId', t.timelineUserId as 't_timelineUserId', t.content as 't_content', -t.spoilerText as 't_spoilerText', t.poll as 't_poll', t.attachments as 't_attachments', -t.provider as 't_provider' -FROM TimelineStatusEntity s -LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId) -LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) -LEFT JOIN StatusViewDataEntity svd ON (s.timelineUserId = svd.timelineUserId AND (s.serverId = svd.serverId OR s.reblogServerId = svd.serverId)) -LEFT JOIN TranslatedStatusEntity t ON (s.timelineUserId = t.timelineUserId AND (s.serverId = t.serverId OR s.reblogServerId = t.serverId)) +SELECT + s.serverId, + s.url, + s.timelineUserId, + s.authorServerId, + s.inReplyToId, + s.inReplyToAccountId, + s.createdAt, + s.editedAt, + s.emojis, + s.reblogsCount, + s.favouritesCount, + s.repliesCount, + s.reblogged, + s.favourited, + s.bookmarked, + s.sensitive, + s.spoilerText, + s.visibility, + s.mentions, + s.tags, + s.application, + s.reblogServerId, + s.reblogAccountId, + s.content, + s.attachments, + s.poll, + s.card, + s.muted, + s.pinned, + s.language, + s.filtered, + a.serverId AS 'a_serverId', + a.timelineUserId AS 'a_timelineUserId', + a.localUsername AS 'a_localUsername', + a.username AS 'a_username', + a.displayName AS 'a_displayName', + a.url AS 'a_url', + a.avatar AS 'a_avatar', + a.emojis AS 'a_emojis', + a.bot AS 'a_bot', + a.createdAt AS 'a_createdAt', + a.limited AS 'a_limited', + a.note AS 'a_note', + rb.serverId AS 'rb_serverId', + rb.timelineUserId AS 'rb_timelineUserId', + rb.localUsername AS 'rb_localUsername', + rb.username AS 'rb_username', + rb.displayName AS 'rb_displayName', + rb.url AS 'rb_url', + rb.avatar AS 'rb_avatar', + rb.emojis AS 'rb_emojis', + rb.bot AS 'rb_bot', + rb.createdAt AS 'rb_createdAt', + rb.limited AS 'rb_limited', + rb.note AS 'rb_note', + svd.serverId AS 'svd_serverId', + svd.timelineUserId AS 'svd_timelineUserId', + svd.expanded AS 'svd_expanded', + svd.contentShowing AS 'svd_contentShowing', + svd.contentCollapsed AS 'svd_contentCollapsed', + svd.translationState AS 'svd_translationState', + t.serverId AS 't_serverId', + t.timelineUserId AS 't_timelineUserId', + t.content AS 't_content', + t.spoilerText AS 't_spoilerText', + t.poll AS 't_poll', + t.attachments AS 't_attachments', + t.provider AS 't_provider' +FROM TimelineStatusEntity AS s +LEFT JOIN TimelineAccountEntity AS a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId) +LEFT JOIN TimelineAccountEntity AS rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) +LEFT JOIN + StatusViewDataEntity AS svd + ON (s.timelineUserId = svd.timelineUserId AND (s.serverId = svd.serverId OR s.reblogServerId = svd.serverId)) +LEFT JOIN + TranslatedStatusEntity AS t + ON (s.timelineUserId = t.timelineUserId AND (s.serverId = t.serverId OR s.reblogServerId = t.serverId)) WHERE s.timelineUserId = :account -ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC""", +ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC +""", ) abstract fun getStatuses(account: Long): PagingSource @@ -89,83 +142,163 @@ ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC""", */ @Query( """ - SELECT rownum - FROM ( - SELECT t1.timelineUserId AS timelineUserId, t1.serverId, COUNT(t2.serverId) - 1 AS rownum - FROM TimelineStatusEntity t1 - JOIN TimelineStatusEntity t2 ON t1.timelineUserId = t2.timelineUserId AND (LENGTH(t1.serverId) <= LENGTH(t2.serverId) AND t1.serverId <= t2.serverId) - WHERE t1.timelineUserId = :pachliAccountId - GROUP BY t1.serverId - ORDER BY length(t1.serverId) DESC, t1.serverId DESC - ) - WHERE serverId = :statusId - """, +SELECT rownum +FROM ( + SELECT + t1.timelineUserId, + t1.serverId, + COUNT(t2.serverId) - 1 AS rownum + FROM TimelineStatusEntity AS t1 + INNER JOIN + TimelineStatusEntity AS t2 + ON + t1.timelineUserId = t2.timelineUserId + AND (LENGTH(t1.serverId) <= LENGTH(t2.serverId) AND t1.serverId <= t2.serverId) + WHERE t1.timelineUserId = :pachliAccountId + GROUP BY t1.serverId + ORDER BY LENGTH(t1.serverId) DESC, t1.serverId DESC +) +WHERE serverId = :statusId +""", ) abstract suspend fun getStatusRowNumber(pachliAccountId: Long, statusId: String): Int @Query( """ -SELECT s.serverId, s.url, s.timelineUserId, -s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt, -s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, -s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId, -s.content, s.attachments, s.poll, s.card, s.muted, s.pinned, s.language, s.filtered, -a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', -a.localUsername as 'a_localUsername', a.username as 'a_username', -a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', -a.emojis as 'a_emojis', a.bot as 'a_bot', a.createdAt as 'a_createdAt', a.limited as 'a_limited', -a.note as 'a_note', -rb.serverId as 'rb_serverId', rb.timelineUserId 'rb_timelineUserId', -rb.localUsername as 'rb_localUsername', rb.username as 'rb_username', -rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar', -rb.emojis as 'rb_emojis', rb.bot as 'rb_bot', rb.createdAt as 'rb_createdAt', rb.limited as 'rb_limited', -rb.note as 'rb_note', -svd.serverId as 'svd_serverId', svd.timelineUserId as 'svd_timelineUserId', -svd.expanded as 'svd_expanded', svd.contentShowing as 'svd_contentShowing', -svd.contentCollapsed as 'svd_contentCollapsed', svd.translationState as 'svd_translationState', -t.serverId as 't_serverId', t.timelineUserId as 't_timelineUserId', t.content as 't_content', -t.spoilerText as 't_spoilerText', t.poll as 't_poll', t.attachments as 't_attachments', -t.provider as 't_provider' -FROM TimelineStatusEntity s -LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId) -LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) -LEFT JOIN StatusViewDataEntity svd ON (s.timelineUserId = svd.timelineUserId AND (s.serverId = svd.serverId OR s.reblogServerId = svd.serverId)) -LEFT JOIN TranslatedStatusEntity t ON (s.timelineUserId = t.timelineUserId AND (s.serverId = t.serverId OR s.reblogServerId = t.serverId)) -WHERE (s.serverId = :statusId OR s.reblogServerId = :statusId) -AND s.authorServerId IS NOT NULL""", +SELECT + s.serverId, + s.url, + s.timelineUserId, + s.authorServerId, + s.inReplyToId, + s.inReplyToAccountId, + s.createdAt, + s.editedAt, + s.emojis, + s.reblogsCount, + s.favouritesCount, + s.repliesCount, + s.reblogged, + s.favourited, + s.bookmarked, + s.sensitive, + s.spoilerText, + s.visibility, + s.mentions, + s.tags, + s.application, + s.reblogServerId, + s.reblogAccountId, + s.content, + s.attachments, + s.poll, + s.card, + s.muted, + s.pinned, + s.language, + s.filtered, + a.serverId AS 'a_serverId', + a.timelineUserId AS 'a_timelineUserId', + a.localUsername AS 'a_localUsername', + a.username AS 'a_username', + a.displayName AS 'a_displayName', + a.url AS 'a_url', + a.avatar AS 'a_avatar', + a.emojis AS 'a_emojis', + a.bot AS 'a_bot', + a.createdAt AS 'a_createdAt', + a.limited AS 'a_limited', + a.note AS 'a_note', + rb.serverId AS 'rb_serverId', + rb.timelineUserId AS 'rb_timelineUserId', + rb.localUsername AS 'rb_localUsername', + rb.username AS 'rb_username', + rb.displayName AS 'rb_displayName', + rb.url AS 'rb_url', + rb.avatar AS 'rb_avatar', + rb.emojis AS 'rb_emojis', + rb.bot AS 'rb_bot', + rb.createdAt AS 'rb_createdAt', + rb.limited AS 'rb_limited', + rb.note AS 'rb_note', + svd.serverId AS 'svd_serverId', + svd.timelineUserId AS 'svd_timelineUserId', + svd.expanded AS 'svd_expanded', + svd.contentShowing AS 'svd_contentShowing', + svd.contentCollapsed AS 'svd_contentCollapsed', + svd.translationState AS 'svd_translationState', + t.serverId AS 't_serverId', + t.timelineUserId AS 't_timelineUserId', + t.content AS 't_content', + t.spoilerText AS 't_spoilerText', + t.poll AS 't_poll', + t.attachments AS 't_attachments', + t.provider AS 't_provider' +FROM TimelineStatusEntity AS s +LEFT JOIN TimelineAccountEntity AS a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId) +LEFT JOIN TimelineAccountEntity AS rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) +LEFT JOIN + StatusViewDataEntity AS svd + ON (s.timelineUserId = svd.timelineUserId AND (s.serverId = svd.serverId OR s.reblogServerId = svd.serverId)) +LEFT JOIN + TranslatedStatusEntity AS t + ON (s.timelineUserId = t.timelineUserId AND (s.serverId = t.serverId OR s.reblogServerId = t.serverId)) +WHERE + (s.serverId = :statusId OR s.reblogServerId = :statusId) + AND s.authorServerId IS NOT NULL +""", ) abstract suspend fun getStatus(statusId: String): TimelineStatusWithAccount? @Query( - """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND - (LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId <= :maxId) -AND -(LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId >= :minId) - """, + """ +DELETE +FROM TimelineStatusEntity +WHERE timelineUserId = :accountId + AND (LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId <= :maxId) + AND (LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId >= :minId) +""", ) abstract suspend fun deleteRange(accountId: Long, minId: String, maxId: String): Int @Query( - """UPDATE TimelineStatusEntity SET favourited = :favourited -WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServerId = :statusId)""", + """ +UPDATE TimelineStatusEntity +SET + favourited = :favourited +WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServerId = :statusId) +""", ) abstract suspend fun setFavourited(pachliAccountId: Long, statusId: String, favourited: Boolean) @Query( - """UPDATE TimelineStatusEntity SET bookmarked = :bookmarked -WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServerId = :statusId)""", + """ +UPDATE TimelineStatusEntity +SET + bookmarked = :bookmarked +WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServerId = :statusId) +""", ) abstract suspend fun setBookmarked(pachliAccountId: Long, statusId: String, bookmarked: Boolean) @Query( - """UPDATE TimelineStatusEntity SET reblogged = :reblogged -WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServerId = :statusId)""", + """ +UPDATE TimelineStatusEntity +SET + reblogged = :reblogged +WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServerId = :statusId) +""", ) abstract suspend fun setReblogged(pachliAccountId: Long, statusId: String, reblogged: Boolean) @Query( - """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :pachliAccountId AND -(authorServerId = :userId OR reblogAccountId = :userId)""", + """ +DELETE +FROM TimelineStatusEntity +WHERE + timelineUserId = :pachliAccountId + AND (authorServerId = :userId OR reblogAccountId = :userId) +""", ) abstract suspend fun removeAllByUser(pachliAccountId: Long, userId: String) @@ -177,26 +310,45 @@ WHERE timelineUserId = :pachliAccountId AND (serverId = :statusId OR reblogServe */ @Query( """ - DELETE FROM TimelineStatusEntity - WHERE timelineUserId = :accountId - AND serverId NOT IN ( - SELECT statusServerId - FROM NotificationEntity - WHERE statusServerId IS NOT NULL - ) - """, +DELETE +FROM TimelineStatusEntity +WHERE + timelineUserId = :accountId + AND serverId NOT IN ( + SELECT statusServerId + FROM NotificationEntity + WHERE statusServerId IS NOT NULL + ) +""", ) abstract suspend fun deleteAllStatusesForAccount(accountId: Long) - @Query("DELETE FROM StatusViewDataEntity WHERE timelineUserId = :accountId") + @Query( + """ +DELETE +FROM StatusViewDataEntity +WHERE timelineUserId = :accountId +""", + ) abstract suspend fun removeAllStatusViewData(accountId: Long) - @Query("DELETE FROM TranslatedStatusEntity WHERE timelineUserId = :accountId") + @Query( + """ +DELETE +FROM TranslatedStatusEntity +WHERE timelineUserId = :accountId +""", + ) abstract suspend fun removeAllTranslatedStatus(accountId: Long) @Query( - """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId -AND serverId = :statusId""", + """ +DELETE +FROM TimelineStatusEntity +WHERE + timelineUserId = :accountId + AND serverId = :statusId +""", ) abstract suspend fun delete(accountId: Long, statusId: String) @@ -223,31 +375,38 @@ AND serverId = :statusId""", @Query( """ DELETE - FROM TimelineStatusEntity - WHERE timelineUserId = :accountId AND serverId IN ( - SELECT serverId FROM ( - WITH statuses(serverId) AS ( +FROM TimelineStatusEntity +WHERE timelineUserId = :accountId AND serverId IN ( + SELECT serverId + FROM ( + WITH statuses (serverId) AS ( -- Statuses that are not associated with a notification. -- Left join with notifications, filter to statuses where the -- join returns a NULL notification ID (because the status has -- no associated notification) SELECT s.serverId - FROM TimelineStatusEntity s - LEFT JOIN NotificationEntity n ON (s.serverId = n.statusServerId AND s.timelineUserId = n.pachliAccountId) - WHERE n.statusServerId IS NULL AND s.timelineUserId = :accountId + FROM TimelineStatusEntity AS s + LEFT JOIN + NotificationEntity AS n + ON (s.serverId = n.statusServerId AND s.timelineUserId = n.pachliAccountId) + WHERE n.statusServerId IS NULL AND s.timelineUserId = :accountId ) + -- Calculate the row number for each row, and exclude rows where -- the row number < limit - SELECT t1.serverId, COUNT(t2.serverId) AS rownum - FROM statuses t1 - LEFT JOIN statuses t2 - ON LENGTH(t1.serverId) < LENGTH(t2.serverId) - OR (LENGTH(t1.serverId) = LENGTH(t2.serverId) AND t1.serverId < t2.serverId) - GROUP BY t1.serverId + SELECT + t1.serverId, + COUNT(t2.serverId) AS rownum + FROM statuses AS t1 + LEFT JOIN statuses AS t2 + ON + LENGTH(t1.serverId) < LENGTH(t2.serverId) + OR (LENGTH(t1.serverId) = LENGTH(t2.serverId) AND t1.serverId < t2.serverId) + GROUP BY t1.serverId HAVING rownum > :keep ) ) - """, +""", ) abstract suspend fun cleanupStatuses(accountId: Long, keep: Int) @@ -258,23 +417,24 @@ DELETE @Query( """ DELETE - FROM TimelineAccountEntity - WHERE timelineUserId = :accountId - AND serverId NOT IN ( - SELECT authorServerId - FROM TimelineStatusEntity - WHERE timelineUserId = :accountId - ) - AND serverId NOT IN ( - SELECT reblogAccountId - FROM TimelineStatusEntity - WHERE timelineUserId = :accountId AND reblogAccountId IS NOT NULL - ) - AND serverId NOT IN ( - SELECT accountServerId - FROM NotificationEntity - WHERE pachliAccountId = :accountId - ) +FROM TimelineAccountEntity +WHERE + timelineUserId = :accountId + AND serverId NOT IN ( + SELECT authorServerId + FROM TimelineStatusEntity + WHERE timelineUserId = :accountId + ) + AND serverId NOT IN ( + SELECT reblogAccountId + FROM TimelineStatusEntity + WHERE timelineUserId = :accountId AND reblogAccountId IS NOT NULL + ) + AND serverId NOT IN ( + SELECT accountServerId + FROM NotificationEntity + WHERE pachliAccountId = :accountId + ) """, ) abstract suspend fun cleanupAccounts(accountId: Long) @@ -284,13 +444,19 @@ DELETE * entries. */ @Query( - """DELETE - FROM StatusViewDataEntity - WHERE timelineUserId = :accountId - AND serverId NOT IN ( - SELECT serverId FROM StatusViewDataEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit - ) - """, + """ +DELETE +FROM StatusViewDataEntity +WHERE + timelineUserId = :accountId + AND serverId NOT IN ( + SELECT serverId + FROM StatusViewDataEntity + WHERE timelineUserId = :accountId + ORDER BY LENGTH(serverId) DESC, serverId DESC + LIMIT :limit + ) +""", ) abstract suspend fun cleanupStatusViewData(accountId: Long, limit: Int) @@ -299,19 +465,29 @@ DELETE * entries. */ @Query( - """DELETE - FROM TranslatedStatusEntity - WHERE timelineUserId = :accountId - AND serverId NOT IN ( - SELECT serverId FROM TranslatedStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit - ) - """, + """ +DELETE +FROM TranslatedStatusEntity +WHERE + timelineUserId = :accountId + AND serverId NOT IN ( + SELECT serverId + FROM TranslatedStatusEntity + WHERE timelineUserId = :accountId + ORDER BY LENGTH(serverId) DESC, serverId DESC + LIMIT :limit + ) +""", ) abstract suspend fun cleanupTranslatedStatus(accountId: Long, limit: Int) @Query( - """UPDATE TimelineStatusEntity SET poll = :poll -WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""", + """ +UPDATE TimelineStatusEntity +SET + poll = :poll +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId) +""", ) abstract suspend fun setVoted(accountId: Long, statusId: String, poll: Poll) @@ -324,10 +500,13 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = * @return Map between serverIds and any cached viewdata for those statuses */ @Query( - """SELECT * - FROM StatusViewDataEntity - WHERE timelineUserId = :accountId - AND serverId IN (:serverIds)""", + """ +SELECT * +FROM StatusViewDataEntity +WHERE + timelineUserId = :accountId + AND serverId IN (:serverIds) +""", ) abstract suspend fun getStatusViewData( accountId: Long, @@ -339,38 +518,69 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = > @Query( - """UPDATE TimelineStatusEntity SET pinned = :pinned -WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""", + """ +UPDATE TimelineStatusEntity +SET + pinned = :pinned +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId) +""", ) abstract suspend fun setPinned(accountId: Long, statusId: String, pinned: Boolean) @Query( - """DELETE FROM TimelineStatusEntity + """ +DELETE +FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IN ( -SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain -AND timelineUserId = :accountId -)""", + SELECT serverId + FROM TimelineAccountEntity + WHERE + username LIKE '%@' || :instanceDomain + AND timelineUserId = :accountId +) +""", ) abstract suspend fun deleteAllFromInstance(accountId: Long, instanceDomain: String) - @Query("UPDATE TimelineStatusEntity SET filtered = NULL WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)") + @Query( + """ +UPDATE TimelineStatusEntity +SET + filtered = NULL +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId) +""", + ) abstract suspend fun clearWarning(accountId: Long, statusId: String): Int - @Query("SELECT COUNT(*) FROM TimelineStatusEntity WHERE timelineUserId = :accountId") + @Query( + """ +SELECT COUNT(*) +FROM TimelineStatusEntity +WHERE timelineUserId = :accountId +""", + ) abstract suspend fun getStatusCount(accountId: Long): Int /** Developer tools: Find N most recent status IDs */ - @Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :count") + @Query( + """ +SELECT serverId +FROM TimelineStatusEntity +WHERE timelineUserId = :accountId +ORDER BY LENGTH(serverId) DESC, serverId DESC +LIMIT :count +""", + ) abstract suspend fun getMostRecentNStatusIds(accountId: Long, count: Int): List /** @returns The [timeline accounts][TimelineAccountEntity] known by [pachliAccountId]. */ @Deprecated("Do not use, only present for tests") @Query( """ - SELECT * - FROM TimelineAccountEntity - WHERE timelineUserId = :pachliAccountId - """, +SELECT * +FROM TimelineAccountEntity +WHERE timelineUserId = :pachliAccountId +""", ) abstract suspend fun loadTimelineAccountsForAccount(pachliAccountId: Long): List } diff --git a/core/database/src/main/kotlin/app/pachli/core/database/dao/TranslatedStatusDao.kt b/core/database/src/main/kotlin/app/pachli/core/database/dao/TranslatedStatusDao.kt index 22045a1f96..a569d96dd3 100644 --- a/core/database/src/main/kotlin/app/pachli/core/database/dao/TranslatedStatusDao.kt +++ b/core/database/src/main/kotlin/app/pachli/core/database/dao/TranslatedStatusDao.kt @@ -32,10 +32,13 @@ interface TranslatedStatusDao { * @return map from statusIDs to known translations for those IDs */ @Query( - """SELECT * - FROM TranslatedStatusEntity - WHERE timelineUserId = :accountId - AND serverId IN (:serverIds)""", + """ +SELECT * +FROM TranslatedStatusEntity +WHERE + timelineUserId = :accountId + AND serverId IN (:serverIds) +""", ) suspend fun getTranslations( accountId: Long, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8119ac5e6d..ef85eee048 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -181,6 +181,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers kotlin-result = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "kotlin-result" } kotlin-result-coroutines = { module = "com.michael-bull.kotlin-result:kotlin-result-coroutines", version.ref = "kotlin-result" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines"} kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines-play-services" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1405e376cd..7054bdd9e0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,7 @@ include(":feature:lists") include(":feature:login") include(":feature:suggestions") include(":tools") +include(":tools:fmtsql") include(":tools:mklanguages") include(":tools:mkserverversions") include(":tools:mvstring") diff --git a/tools/fmtsql/README.md b/tools/fmtsql/README.md new file mode 100644 index 0000000000..0251afe78e --- /dev/null +++ b/tools/fmtsql/README.md @@ -0,0 +1,30 @@ +# fmtsql + +## Synopsis + +`fmtsql` formats the SQL in `@Query` annotations using `sqlfluff`, which must be installed on your system. + +## Usage + +From the parent directory, run: + +```shell +./runtools fmtsql +``` + +The result may not be 100% ktlint compatible, so then run: + +```shell +./gradlew ktlintformat +``` + +Verify the modifications made to the DAO files, and commit the result. + +## Options + +- `--dir`: Path to the DAO files containing `@Query` annotations. The default is the path to Pachli DAO files +- `--sqlfluff`: Full path to the `sqlfluff` executable + +## Configuration + +SQL formatting is controlled by the `.sqlfluff` configuration file in the project's root directory. diff --git a/tools/fmtsql/build.gradle.kts b/tools/fmtsql/build.gradle.kts new file mode 100644 index 0000000000..ffde2f4428 --- /dev/null +++ b/tools/fmtsql/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +plugins { + alias(libs.plugins.google.ksp) + alias(libs.plugins.pachli.tool) +} + +application { + mainClass = "app.pachli.fmtsql.MainKt" +} + +dependencies { + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.moshi) + ksp(libs.moshi.codegen) + + implementation(libs.kotlin.result) +} diff --git a/tools/fmtsql/src/main/kotlin/app/pachli/fmtsql/Main.kt b/tools/fmtsql/src/main/kotlin/app/pachli/fmtsql/Main.kt new file mode 100644 index 0000000000..5472f9cc06 --- /dev/null +++ b/tools/fmtsql/src/main/kotlin/app/pachli/fmtsql/Main.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2025 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +package app.pachli.fmtsql + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.command.main +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.file +import com.github.ajalt.clikt.parameters.types.path +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.util.concurrent.TimeUnit +import kotlin.io.path.Path +import kotlin.io.path.createTempFile +import kotlin.io.path.isRegularFile +import kotlin.io.path.listDirectoryEntries +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext + +/** + * Lint result from sqlfluff for a single file. + * + * @param violations Individual lint violations. + */ +@JsonClass(generateAdapter = true) +data class SqlFluffLintResult( + val violations: List, +) + +/** + * Individual sqlfluff lint violation. + * + * @param code The violated rule. + * @param description End-user description of the rule. + * @param name Internal name of the rule. + * @param fixes List of fixes that can be applied. Empty if no fixes are possible. + */ +@JsonClass(generateAdapter = true) +data class SqlFluffViolation( + val code: String, + val description: String, + val name: String, + val fixes: List?, +) + +/** Formats SQL queries in `@Query(...)` annotations using `sqlfluff` */ +class App : SuspendingCliktCommand() { + override fun help(context: Context) = "Run sqlfluff on DAO SQL" + + /** + * Regex to match SQL in `@Query` annotations. + * + * Matches `@Query("...")` and `@Query("""...""")` where the content inside + * may span multiple lines. + */ + private val rxQuery = """@Query\(\s*"{1,3}(.*?)"{1,3},?\s*\)""".toRegex( + setOf( + RegexOption.DOT_MATCHES_ALL, + RegexOption.MULTILINE, + ), + ) + private val moshi: Moshi = Moshi.Builder().build() + + @OptIn(ExperimentalStdlibApi::class) + val lintResultAdapter = moshi.adapter>() + + private val daoDir by option("--dir", help = "Path to directory containing DAO files") + .path(mustExist = true) + .default(Path("core/database/src/main/kotlin/app/pachli/core/database/dao")) + private val sqlFluff by option("--sqlfluff", help = "Path to sqlfluff executable") + .file(mustExist = true) + .default(File("""C:\Users\Nik\AppData\Local\Programs\Python\Python313\Scripts\sqlfluff.exe""")) + + override suspend fun run() = coroutineScope { + System.setProperty("file.encoding", "UTF8") + + val jobs = daoDir.listDirectoryEntries("*.kt") + .filter { it.isRegularFile() } + .map { async { formatSql(it.toFile()) } } + + jobs.awaitAll() + return@coroutineScope + } + + /** Format `@Query` annotations in [file]. */ + private suspend fun formatSql(file: File) = withContext(Dispatchers.IO) { + println(file.path) + + val content = file.readText() + + val newContent = rxQuery.replace(content) { match -> + val unformattedSql = match.groupValues[1].trim() + val lintResults = sqlfluffLint(unformattedSql) + + if (lintResults == null) { + println("error: could not parse lint results for query") + println(" sql: $unformattedSql") + return@replace match.value + } + + val violations = lintResults.first().violations + + // No violations? Nothing to format. + if (violations.isEmpty()) return@replace match.value + + // If any violations have no fixes then treat as unfixable. + violations.filter { it.fixes.isNullOrEmpty() }.takeIf { it.isNotEmpty() }?.let { unfixable -> + println("error: $file: can't fix query, it has unfixable lint violations") + println(" sql: $unformattedSql") + unfixable.forEach { println(" $it") } + return@replace match.value + } + + // Format, and either return the formatted value (if OK) or the + // original value (if an error occurred). + return@replace when (val result = sqlfluffFix(unformattedSql)) { + is Err -> { + println("error: $file: ${result.error}") + println(" sql: $unformattedSql") + match.value + } + is Ok -> result.value + } + } + + // No changes? + if (newContent == content) return@withContext + + val tmpFile = createTempFile().toFile() + val tmpW = tmpFile.printWriter() + + tmpW.print(newContent) + tmpW.close() + Files.copy(tmpFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + + /** + * Lint's [sql] with sqlfluff. + * + * @return Array of [SqlFluffLintResult], null if the SQL could not be + * linted. + */ + private fun sqlfluffLint(sql: String): Array? { + val cmd = arrayOf(sqlFluff.path, "lint", "-f", "json", "-") + + val lint = ProcessBuilder(*cmd) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .start() + lint.outputWriter().use { w -> + w.append(sql) + w.newLine() + w.flush() + } + lint.waitFor(10, TimeUnit.SECONDS) + return lintResultAdapter.fromJson(lint.inputStream.bufferedReader().readText()) + } + + /** + * Formats [sql] with `sqlfluff fix`. + * + * @return A `Result` with either the formatted string, or errors that + * occurred during formatting. + */ + private fun sqlfluffFix(sql: String): Result { + val cmdFix = arrayOf(sqlFluff.path, "fix", "-") + val proc = ProcessBuilder(*cmdFix) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + val w = proc.outputWriter() + w.append(sql) + w.newLine() + w.flush() + w.close() + + proc.waitFor(10, TimeUnit.SECONDS) + val errors = proc.errorStream.bufferedReader().readText() + if (errors.isNotEmpty()) return Err(errors) + + val formattedSql = proc.inputStream.bufferedReader().readText().trim() + val tq = "\"\"\"" + return Ok( + """@Query( + $tq +$formattedSql +$tq, + )""", + ) + } +} + +suspend fun main(args: Array) = App().main(args)