From ce3dd8b7e7c0286594722b19c20053905adbbe2e Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 3 Jun 2021 00:41:58 +0530 Subject: [PATCH] database: initial commit Signed-off-by: Harsh Shandilya --- .idea/artifacts/database_jvm.xml | 8 + database/.gitignore | 1 + database/build.gradle.kts | 42 +++++ database/consumer-rules.pro | 1 + database/src/androidMain/AndroidManifest.xml | 4 + .../msfjarvis/lobsters/data/local/Database.kt | 12 ++ .../msfjarvis/lobsters/data/local/Database.kt | 18 +++ .../lobsters/data/model/TagsAdapter.kt | 17 ++ .../lobsters/data/local/SavedPost.sq | 39 +++++ .../msfjarvis/lobsters/data/local/Database.kt | 13 ++ .../data/local/SqlDelightQueriesTest.kt | 153 ++++++++++++++++++ gradle/libs.versions.toml | 4 + settings.gradle.kts | 2 + 13 files changed, 314 insertions(+) create mode 100644 .idea/artifacts/database_jvm.xml create mode 100644 database/.gitignore create mode 100644 database/build.gradle.kts create mode 100644 database/consumer-rules.pro create mode 100644 database/src/androidMain/AndroidManifest.xml create mode 100644 database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt create mode 100644 database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt create mode 100644 database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt create mode 100644 database/src/commonMain/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq create mode 100644 database/src/desktopMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt create mode 100644 database/src/desktopTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt diff --git a/.idea/artifacts/database_jvm.xml b/.idea/artifacts/database_jvm.xml new file mode 100644 index 00000000..e1faad6e --- /dev/null +++ b/.idea/artifacts/database_jvm.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/database/build/libs + + + + + \ No newline at end of file diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +/build diff --git a/database/build.gradle.kts b/database/build.gradle.kts new file mode 100644 index 00000000..957ee5cc --- /dev/null +++ b/database/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") + id("com.squareup.sqldelight") version "1.5.0" +} + +kotlin { + android() + jvm("desktop") { compilations.all { kotlinOptions.jvmTarget = "11" } } + sourceSets { + val commonMain by getting + val commonTest by getting + val androidMain by getting { + dependencies { implementation(libs.thirdparty.sqldelight.androidDriver) } + } + val androidTest by getting + val desktopMain by getting { dependencies { implementation(libs.thirdparty.sqldelight.jvmDriver) } } + val desktopTest by getting { + dependencies { + implementation(libs.kotlin.coroutines.core) + implementation(kotlin("test-junit")) + } + } + } +} + +android { + compileSdkVersion(30) + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdkVersion(23) + targetSdkVersion(30) + consumerProguardFiles("consumer-rules.pro") + } +} + +configure { + database("LobstersDatabase") { + packageName = "dev.msfjarvis.lobsters.database" + sourceFolders = listOf("sqldelight") + } +} diff --git a/database/consumer-rules.pro b/database/consumer-rules.pro new file mode 100644 index 00000000..a4b76f7c --- /dev/null +++ b/database/consumer-rules.pro @@ -0,0 +1 @@ +-keep class dev.msfjarvis.lobsters.model.** { *; } diff --git a/database/src/androidMain/AndroidManifest.xml b/database/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..1ac3d442 --- /dev/null +++ b/database/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + diff --git a/database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt b/database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt new file mode 100644 index 00000000..37209b72 --- /dev/null +++ b/database/src/androidMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt @@ -0,0 +1,12 @@ +package dev.msfjarvis.lobsters.data.local + +import android.content.Context +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver +import dev.msfjarvis.lobsters.database.LobstersDatabase + +actual class DriverFactory(private val context: Context) { + actual fun createDriver(): SqlDriver { + return AndroidSqliteDriver(LobstersDatabase.Schema, context, LobstersDatabaseName) + } +} diff --git a/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt new file mode 100644 index 00000000..1ab3266e --- /dev/null +++ b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt @@ -0,0 +1,18 @@ +package dev.msfjarvis.lobsters.data.local + +import com.squareup.sqldelight.db.SqlDriver +import dev.msfjarvis.lobsters.data.model.TagsAdapter +import dev.msfjarvis.lobsters.database.LobstersDatabase + +internal const val LobstersDatabaseName = "SavedPosts.db" + +expect class DriverFactory { + fun createDriver(): SqlDriver +} + +private fun getTagsAdapter() = TagsAdapter() + +fun createDatabase(driverFactory: DriverFactory): LobstersDatabase { + val driver = driverFactory.createDriver() + return LobstersDatabase(driver, SavedPost.Adapter(getTagsAdapter())) +} diff --git a/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt new file mode 100644 index 00000000..3b8fcb48 --- /dev/null +++ b/database/src/commonMain/kotlin/dev/msfjarvis/lobsters/data/model/TagsAdapter.kt @@ -0,0 +1,17 @@ +package dev.msfjarvis.lobsters.data.model + +import com.squareup.sqldelight.ColumnAdapter + +class TagsAdapter : ColumnAdapter, String> { + override fun decode(databaseValue: String): List { + return databaseValue.split(SEPARATOR) + } + + override fun encode(value: List): String { + return value.joinToString(SEPARATOR) + } + + private companion object { + private const val SEPARATOR = "," + } +} diff --git a/database/src/commonMain/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq b/database/src/commonMain/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq new file mode 100644 index 00000000..b1977c08 --- /dev/null +++ b/database/src/commonMain/sqldelight/dev/msfjarvis/lobsters/data/local/SavedPost.sq @@ -0,0 +1,39 @@ +import kotlin.collections.List; + +CREATE TABLE IF NOT EXISTS SavedPost( + shortId TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + url TEXT NOT NULL, + createdAt TEXT NOT NULL, + commentsUrl TEXT NOT NULL, + submitterName TEXT NOT NULL, + submitterAvatarUrl TEXT NOT NULL, + tags TEXT AS List NOT NULL +); + +insertOrReplacePost: +INSERT OR REPLACE +INTO SavedPost +VALUES ?; + +selectAllPosts: +SELECT * +FROM SavedPost; + +selectCount: +SELECT COUNT(*) +FROM SavedPost; + +deleteAllPosts: +DELETE +FROM SavedPost; + +deletePost: +DELETE +FROM SavedPost +WHERE shortId = ?; + +selectPost: +SELECT * +FROM SavedPost +WHERE shortId = ?; diff --git a/database/src/desktopMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt b/database/src/desktopMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt new file mode 100644 index 00000000..9883a8a3 --- /dev/null +++ b/database/src/desktopMain/kotlin/dev/msfjarvis/lobsters/data/local/Database.kt @@ -0,0 +1,13 @@ +package dev.msfjarvis.lobsters.data.local + +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver +import dev.msfjarvis.lobsters.database.LobstersDatabase + +actual class DriverFactory { + actual fun createDriver(): SqlDriver { + val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + LobstersDatabase.Schema.create(driver) + return driver + } +} diff --git a/database/src/desktopTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt b/database/src/desktopTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt new file mode 100644 index 00000000..5ca12981 --- /dev/null +++ b/database/src/desktopTest/kotlin/dev/msfjarvis/lobsters/data/local/SqlDelightQueriesTest.kt @@ -0,0 +1,153 @@ +package dev.msfjarvis.lobsters.data.local + +import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver +import dev.msfjarvis.lobsters.data.model.TagsAdapter +import dev.msfjarvis.lobsters.database.LobstersDatabase +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Before + +@OptIn(ExperimentalStdlibApi::class) +class SqlDelightQueriesTest { + + private lateinit var postQueries: SavedPostQueries + + @Before + fun setUp() { + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + LobstersDatabase.Schema.create(driver) + val database = + LobstersDatabase( + driver, + SavedPost.Adapter(TagsAdapter()), + ) + postQueries = database.savedPostQueries + } + + @Test + fun selectCount() = runBlocking { + val posts = createTestData(5) + + posts.forEach { postQueries.insertOrReplacePost(it) } + + val postCount = postQueries.selectCount().executeAsOne() + assertEquals(5, postCount) + } + + @Test + fun insertIntoDatabase() = runBlocking { + // Get 5 posts + val posts = createTestData(5) + + // Insert posts into DB + posts.forEach { postQueries.insertOrReplacePost(it) } + + // Check post count + val postsCount = postQueries.selectCount().executeAsOne() + assertEquals(5, postsCount) + } + + @Test + fun replaceFromDatabase() = runBlocking { + // Get 1 post + val post = createTestData(1)[0] + + // Insert post into DB + postQueries.insertOrReplacePost(post) + + // Create a new post and try replacing it + val newPost = post.copy(submitterName = "Fake name") + postQueries.insertOrReplacePost(newPost) + + // Check post count + val postsCount = postQueries.selectCount().executeAsOne() + assertEquals(1, postsCount) + + // Check if post is updated + val postFromDb = postQueries.selectPost(post.shortId).executeAsOne() + assertEquals("Fake name", postFromDb.submitterName) + } + + @Test + fun selectPost() = runBlocking { + // Get 1 post + val post = createTestData(1)[0] + + // Insert post into DB + postQueries.insertOrReplacePost(post) + + val postFromDb = postQueries.selectAllPosts().executeAsOne() + assertEquals("test_id_1", postFromDb.shortId) + } + + @Test + fun selectAllPosts() = runBlocking { + // Get 5 post + val posts = createTestData(5) + + // Insert posts into DB + posts.forEach { postQueries.insertOrReplacePost(it) } + + val postsFromDb = postQueries.selectAllPosts().executeAsList() + + // Check if all posts have correct shortId + for (i in 1..5) { + assertEquals("test_id_$i", postsFromDb[i - 1].shortId) + } + } + + @Test + fun deletePost() = runBlocking { + // Create 3 posts and insert them to DB + val posts = createTestData(3) + posts.forEach { postQueries.insertOrReplacePost(it) } + + // Delete 2nd post + postQueries.deletePost("test_id_2") + + val postsFromDB = postQueries.selectAllPosts().executeAsList() + + // Check if size is 2, and only the correct post is deleted + assertEquals(2, postsFromDB.size) + assertEquals("test_id_1", postsFromDB[0].shortId) + assertEquals("test_id_3", postsFromDB[1].shortId) + } + + @Test + fun deleteAllPost() = runBlocking { + // Create 5 posts and insert them to DB + val posts = createTestData(5) + posts.forEach { postQueries.insertOrReplacePost(it) } + + // Delete all posts + postQueries.deleteAllPosts() + + val postsCount = postQueries.selectCount().executeAsOne() + + // Check if db is empty + assertEquals(0, postsCount) + } + + private fun createTestData(count: Int): ArrayList { + val posts = arrayListOf() + + for (i in 1..count) { + val post = + SavedPost( + shortId = "test_id_$i", + createdAt = "0", + title = "test", + url = "test_url", + commentsUrl = "test_comments_url", + submitterName = "test_user_$i", + submitterAvatarUrl = "test_avatar_url", + tags = listOf(), + ) + + posts.add(post) + } + + return posts + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07ab6c14..1d86de8c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ coroutines = "1.5.0" kotlin = "1.5.10" moshix = "0.11.2" retrofit = "2.9.0" +sqldelight = "1.5.0" [libraries] @@ -17,5 +18,8 @@ thirdparty-moshix-metadatareflect = { module = "dev.zacsweers.moshix:moshi-metad thirdparty-retrofit-lib = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } thirdparty-retrofit-moshiConverter = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } +thirdparty-sqldelight-jvmDriver = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } +thirdparty-sqldelight-androidDriver = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } + testing-kotlintest-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } testing-mockWebServer = "com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.2" diff --git a/settings.gradle.kts b/settings.gradle.kts index f916bc25..db9eacb8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,4 +19,6 @@ include(":api") include(":common") +include(":database") + include(":desktop")