data: Add a composite table to hold Article tags relations
authorDa Risk <da_risk@geekorum.com>
Sat, 09 May 2020 22:38:05 -0400
changeset 674 825fa5987f3c
parent 673 b25358bd9512
child 675 3b1c8ed2b014
data: Add a composite table to hold Article tags relations
app/src/androidTest/java/com/geekorum/ttrss/data/migrations/ArticlesDatabaseMigrationTest.kt
app/src/androidTest/java/com/geekorum/ttrss/sync/workers/mocks.kt
app/src/main/java/com/geekorum/ttrss/data/ArticlesDatabase.kt
app/src/main/java/com/geekorum/ttrss/data/SynchronizationDao.kt
app/src/main/java/com/geekorum/ttrss/data/Types.kt
app/src/main/java/com/geekorum/ttrss/data/migrations/ArticlesDatabase.kt
app/src/main/java/com/geekorum/ttrss/data/plugins/SynchronizationFacade.kt
app/src/main/java/com/geekorum/ttrss/sync/DatabaseService.kt
app/src/main/java/com/geekorum/ttrss/sync/workers/CollectNewArticlesWorker.kt
--- a/app/src/androidTest/java/com/geekorum/ttrss/data/migrations/ArticlesDatabaseMigrationTest.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/androidTest/java/com/geekorum/ttrss/data/migrations/ArticlesDatabaseMigrationTest.kt	Sat May 09 22:38:05 2020 -0400
@@ -249,6 +249,15 @@
         }
     }
 
+    private fun assertMigration12To13DataIntegrity(db: SupportSQLiteDatabase) {
+        assertMigration7To8DataIntegrity(db)
+        db.query("SELECT * FROM ${Tables.ARTICLES_TAGS}").use {
+            assertThat(it.count).isEqualTo(1)
+            it.moveToFirst()
+            assertThat(it.getValue<String>("tag")).isEqualTo("article tags")
+        }
+    }
+
     private fun createSomeArticlesFromVersion8(db: SupportSQLiteDatabase) {
         var values = contentValuesOf(
             ArticlesContract.Category.TITLE to "category",
@@ -416,6 +425,24 @@
         }
     }
 
+    @Test
+    fun migrate12To13() {
+        helper.createDatabase(TEST_DB, 12).use {
+            // db has schema version 8. insert some contentData using SQL queries.
+            // You cannot use DAO classes because they expect the latest schema.
+            // as our schema for this migration doesn't change much from the previous
+            // we can reuse the same function
+            createSomeArticlesFromVersion10(it)
+        }
+
+        helper.runMigrationsAndValidate(TEST_DB, 13, true,
+            *ALL_MIGRATIONS.toTypedArray()).use {
+            // MigrationTestHelper automatically verifies the schema changes,
+            // but you need to validate that the contentData was migrated properly.
+            assertMigration12To13DataIntegrity(it)
+        }
+    }
+
 
     private inline fun <reified T> Cursor.getValue(columnName: String) : T {
         val index = getColumnIndexOrThrow(columnName)
--- a/app/src/androidTest/java/com/geekorum/ttrss/sync/workers/mocks.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/androidTest/java/com/geekorum/ttrss/sync/workers/mocks.kt	Sat May 09 22:38:05 2020 -0400
@@ -23,6 +23,7 @@
 import com.geekorum.ttrss.data.AccountInfo
 import com.geekorum.ttrss.data.Article
 import com.geekorum.ttrss.data.ArticleWithAttachments
+import com.geekorum.ttrss.data.ArticlesTags
 import com.geekorum.ttrss.data.Attachment
 import com.geekorum.ttrss.data.Category
 import com.geekorum.ttrss.data.Feed
@@ -75,6 +76,7 @@
     private val articles = mutableListOf<Article>()
     private val attachments = mutableListOf<Attachment>()
     private val transactions = mutableListOf<Transaction>()
+    private val articlesTags = mutableListOf<ArticlesTags>()
 
     override suspend fun <R> runInTransaction(block: suspend () -> R) {
         block()
@@ -132,6 +134,10 @@
         this.articles.addAll(articles)
     }
 
+    override suspend fun insertArticleTags(articlesTags: List<ArticlesTags>) {
+        this.articlesTags.addAll(articlesTags)
+    }
+
     override suspend fun updateArticle(article: Article) {
         val present = articles.first {
             it.id == article.id
--- a/app/src/main/java/com/geekorum/ttrss/data/ArticlesDatabase.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/data/ArticlesDatabase.kt	Sat May 09 22:38:05 2020 -0400
@@ -24,9 +24,9 @@
 import androidx.room.RoomDatabase
 import com.geekorum.ttrss.providers.PurgeArticlesDao
 
-@Database(entities = [Article::class, ArticleFTS::class, Attachment::class,
+@Database(entities = [Article::class, ArticleFTS::class, ArticlesTags::class, Attachment::class,
     Category::class, Feed::class, Transaction::class, AccountInfo::class],
-        version = 12)
+        version = 13)
 abstract class ArticlesDatabase : RoomDatabase() {
     abstract fun articleDao(): ArticleDao
     abstract fun accountInfoDao(): AccountInfoDao
@@ -45,5 +45,6 @@
         const val TRANSACTIONS = "transactions"
         const val FEEDS = "feeds"
         const val CATEGORIES = "categories"
+        const val ARTICLES_TAGS = "articles_tags"
     }
 }
--- a/app/src/main/java/com/geekorum/ttrss/data/SynchronizationDao.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/data/SynchronizationDao.kt	Sat May 09 22:38:05 2020 -0400
@@ -89,6 +89,9 @@
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     abstract suspend fun insertArticles(dataArticles: List<Article>)
 
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    abstract suspend fun insertArticlesTags(articlesTags: List<ArticlesTags>)
+
     @Update(entity = Article::class)
     abstract suspend fun updateArticlesMetadata(metadata: List<Metadata>)
 
--- a/app/src/main/java/com/geekorum/ttrss/data/Types.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/data/Types.kt	Sat May 09 22:38:05 2020 -0400
@@ -26,6 +26,8 @@
 import androidx.room.Entity
 import androidx.room.ForeignKey
 import androidx.room.Fts4
+import androidx.room.Index
+import androidx.room.Junction
 import androidx.room.PrimaryKey
 import androidx.room.Relation
 import java.text.DateFormat
@@ -234,6 +236,28 @@
     var unreadCount: Int = 0
 )
 
+@Entity(tableName = "articles_tags", primaryKeys = ["tag", "article_id"],
+    foreignKeys = [ForeignKey(
+        entity = Article::class,
+        parentColumns = ["_id"],
+        childColumns = ["article_id"],
+        onDelete = ForeignKey.CASCADE
+    )])
+data class ArticlesTags(
+    @ColumnInfo(name = "article_id", index = true)
+    val articleId: Long,
+    @ColumnInfo(index = true)
+    val tag: String
+)
+
+data class TagWithArticles(
+    val tag: String,
+//    val articleId: Long,
+    @Relation(parentColumn = "tag", entityColumn = "_id",
+    associateBy = Junction(ArticlesTags::class, entityColumn = "article_id"))
+    val article: List<Article>
+)
+
 
 @Entity(tableName = "feeds",
         foreignKeys = [ForeignKey(entity = Category::class,
--- a/app/src/main/java/com/geekorum/ttrss/data/migrations/ArticlesDatabase.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/data/migrations/ArticlesDatabase.kt	Sat May 09 22:38:05 2020 -0400
@@ -504,6 +504,47 @@
 }
 
 
+/**
+ * This migration adds a relation table for articles_tags
+ */
+object MigrationFrom12To13 : Migration(12, 13) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        addArticleTagsTable(database)
+        migrateArticleTags(database)
+    }
+
+    private fun addArticleTagsTable(database: SupportSQLiteDatabase) {
+        with(database) {
+            execSQL("""CREATE TABLE IF NOT EXISTS `articles_tags` (
+                |`article_id` INTEGER NOT NULL,
+                |`tag` TEXT NOT NULL,
+                |PRIMARY KEY(`tag`, `article_id`)
+                |FOREIGN KEY(`article_id`) REFERENCES `articles`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE
+                |)""".trimMargin())
+            execSQL("CREATE INDEX IF NOT EXISTS `index_articles_tags_article_id` ON `articles_tags` (`article_id`)")
+            execSQL("CREATE INDEX IF NOT EXISTS `index_articles_tags_tag` ON `articles_tags` (`tag`)")
+        }
+    }
+
+    private fun migrateArticleTags(database: SupportSQLiteDatabase) {
+        with(database) {
+            query("SELECT _id, tags FROM articles").use {
+                while (it.moveToNext()) {
+                    val articleId = it.getLong(0)
+                    val tags = it.getString(1)
+                    val tagsList = tags.split(",")
+                        .map(String::trim)
+                        .filter(String::isNotEmpty)
+                        .distinct()
+                    for (tag in tagsList) {
+                        execSQL("INSERT INTO `articles_tags` (`article_id`, `tag`) VALUES (?, ?)", arrayOf(articleId, tag))
+                    }
+                }
+            }
+        }
+    }
+}
+
 internal val ALL_MIGRATIONS = listOf(MigrationFrom1To2,
         MigrationFrom2To3,
         MigrationFrom3To4,
@@ -514,4 +555,5 @@
         MigrationFrom8To9,
         MigrationFrom9To10,
         MigrationFrom10To11,
-        MigrationFrom11To12)
+        MigrationFrom11To12,
+        MigrationFrom12To13)
--- a/app/src/main/java/com/geekorum/ttrss/data/plugins/SynchronizationFacade.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/data/plugins/SynchronizationFacade.kt	Sat May 09 22:38:05 2020 -0400
@@ -25,6 +25,7 @@
 import com.geekorum.ttrss.data.AccountInfoDao
 import com.geekorum.ttrss.data.Article
 import com.geekorum.ttrss.data.ArticlesDatabase
+import com.geekorum.ttrss.data.ArticlesTags
 import com.geekorum.ttrss.data.Attachment
 import com.geekorum.ttrss.data.Category
 import com.geekorum.ttrss.data.Feed
@@ -62,6 +63,9 @@
     override suspend fun getRandomArticleFromFeed(feedId: Long): Article? = synchronizationDao.getArticleFromFeed(feedId)
 
     override suspend fun insertArticles(articles: List<Article>) = synchronizationDao.insertArticles(articles)
+    override suspend fun insertArticleTags(articlesTags: List<ArticlesTags>) {
+        synchronizationDao.insertArticlesTags(articlesTags)
+    }
 
     override suspend fun getCategories(): List<Category> = synchronizationDao.getAllCategories()
 
--- a/app/src/main/java/com/geekorum/ttrss/sync/DatabaseService.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/sync/DatabaseService.kt	Sat May 09 22:38:05 2020 -0400
@@ -22,6 +22,7 @@
 
 import com.geekorum.ttrss.data.AccountInfo
 import com.geekorum.ttrss.data.Article
+import com.geekorum.ttrss.data.ArticlesTags
 import com.geekorum.ttrss.data.Attachment
 import com.geekorum.ttrss.data.Category
 import com.geekorum.ttrss.data.Feed
@@ -49,6 +50,7 @@
     suspend fun getArticle(id: Long): Article?
     suspend fun getRandomArticleFromFeed(feedId: Long): Article?
     suspend fun insertArticles(articles: List<Article>)
+    suspend fun insertArticleTags(articlesTags: List<ArticlesTags>)
     suspend fun updateArticle(article: Article)
     suspend fun getLatestArticleId(): Long?
     suspend fun getLatestArticleIdFromFeed(feedId: Long): Long?
--- a/app/src/main/java/com/geekorum/ttrss/sync/workers/CollectNewArticlesWorker.kt	Wed May 13 11:26:44 2020 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/sync/workers/CollectNewArticlesWorker.kt	Sat May 09 22:38:05 2020 -0400
@@ -32,6 +32,7 @@
 import com.geekorum.ttrss.core.CoroutineDispatchersProvider
 import com.geekorum.ttrss.data.Article
 import com.geekorum.ttrss.data.ArticleWithAttachments
+import com.geekorum.ttrss.data.ArticlesTags
 import com.geekorum.ttrss.htmlparsers.ImageUrlExtractor
 import com.geekorum.ttrss.network.ApiService
 import com.geekorum.ttrss.sync.BackgroundDataUsageManager
@@ -156,7 +157,15 @@
     }
 
     private suspend fun insertArticles(articles: List<ArticleWithAttachments>) {
-        databaseService.insertArticles(articles.map { it.article })
+        val articlesOnly = articles.map { it.article }
+        databaseService.insertArticles(articlesOnly)
+        val articlesTags = articlesOnly.flatMap {
+            val tags = it.tags.split(",").map(String::trim)
+            tags.map {tag ->
+                ArticlesTags(it.id, tag)
+            }
+        }
+        databaseService.insertArticleTags(articlesTags)
         val attachments = articles.flatMap { it.attachments }
         databaseService.insertAttachments(attachments)
     }