diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d7de03327..b19566594cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.4.4 + +* `Versions`: + * `Klock`: `1.12.1` -> `2.0.0` +* `Repos`: + * Add interface `VersionsRepo` + * Add default realization of `VersionsRepo` named `StandardVersionsRepo` which use `StandardVersionsRepoProxy` + to get access to some end-store + * Add default realization of `StandardVersionsRepoProxy` based on `KeyValue` repos + * Add realizations of `StandardVersionsRepoProxy` for exposed and android (`SQL` and `SharedPreferences`) + * `Commons`: + * In Android fully reworked transactions functions + * Now `DatabaseCoroutineContext` is a shortcut for `Dispatchers.IO` + ## 0.4.3 * `Versions`: @@ -244,4 +258,4 @@ All deprecations has been removed ## 0.1.0 -Inited :) \ No newline at end of file +Inited :) diff --git a/build.gradle b/build.gradle index c6d3e1911c6..07759c6e7bf 100644 --- a/build.gradle +++ b/build.gradle @@ -8,10 +8,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version" classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" diff --git a/gradle.properties b/gradle.properties index 334d0f94298..0e57629edc6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,9 +14,8 @@ kotlin_exposed_version=0.28.1 ktor_version=1.4.2 -klockVersion=1.12.1 +klockVersion=2.0.0 -gradle_bintray_plugin_version=1.8.5 github_release_plugin_version=2.2.12 uuidVersion=0.2.2 @@ -41,5 +40,5 @@ dokka_version=1.4.0 # Project data group=dev.inmo -version=0.4.3 -android_code_version=7 +version=0.4.4 +android_code_version=8 diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/KeyValueBasedVersionsRepoProxy.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/KeyValueBasedVersionsRepoProxy.kt new file mode 100644 index 00000000000..5d7e3435670 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/KeyValueBasedVersionsRepoProxy.kt @@ -0,0 +1,13 @@ +package dev.inmo.micro_utils.repos.versions + +import dev.inmo.micro_utils.repos.StandardKeyValueRepo +import dev.inmo.micro_utils.repos.set + +class KeyValueBasedVersionsRepoProxy( + private val keyValueStore: StandardKeyValueRepo, + override val database: T +) : StandardVersionsRepoProxy { + override suspend fun getTableVersion(tableName: String): Int? = keyValueStore.get(tableName) + + override suspend fun updateTableVersion(tableName: String, version: Int) { keyValueStore.set(tableName, version) } +} diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/StandardVersionsRepo.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/StandardVersionsRepo.kt new file mode 100644 index 00000000000..b02432c2759 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/StandardVersionsRepo.kt @@ -0,0 +1,36 @@ +package dev.inmo.micro_utils.repos.versions + +import dev.inmo.micro_utils.repos.Repo + +interface StandardVersionsRepoProxy : Repo { + val database: T + + suspend fun getTableVersion(tableName: String): Int? + suspend fun updateTableVersion(tableName: String, version: Int) +} + +class StandardVersionsRepo( + private val proxy: StandardVersionsRepoProxy +) : VersionsRepo { + override suspend fun setTableVersion( + tableName: String, + version: Int, + onCreate: suspend T.() -> Unit, + onUpdate: suspend T.(from: Int, to: Int) -> Unit + ) { + var savedVersion = proxy.getTableVersion(tableName) + if (savedVersion == null) { + proxy.database.onCreate() + proxy.updateTableVersion(tableName, version) + } else { + while (savedVersion != null && savedVersion < version) { + val newVersion = savedVersion + 1 + + proxy.database.onUpdate(savedVersion, newVersion) + + proxy.updateTableVersion(tableName, newVersion) + savedVersion = newVersion + } + } + } +} \ No newline at end of file diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/VersionsRepo.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/VersionsRepo.kt new file mode 100644 index 00000000000..ee7cbab700f --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/versions/VersionsRepo.kt @@ -0,0 +1,31 @@ +package dev.inmo.micro_utils.repos.versions + +import dev.inmo.micro_utils.repos.Repo + +/** + * This interface has been created due to requirement to work with different versions of databases and make some + * migrations between versions + * + * @param T It is a type of database, which will be used by this repo to retrieve current table version and update it + */ +interface VersionsRepo : Repo { + /** + * By default, instance of this interface will check that version of table with name [tableName] is less than + * [version] or is absent + * + * * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version] + * * In case if [tableName] have version less than parameter [version], it will increase version one-by-one + * until database version will be equal to [version] + * + * @param version Current version of table + * @param onCreate This callback will be called in case when table have no information about table + * @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter + * "to" will always be greater than "from" + */ + suspend fun setTableVersion( + tableName: String, + version: Int, + onCreate: suspend T.() -> Unit = {}, + onUpdate: suspend T.(from: Int, to: Int) -> Unit = { _, _ ->} + ) +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt index 8a2932eb0fe..b99835e5972 100644 --- a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt @@ -1,6 +1,6 @@ package dev.inmo.micro_utils.repos -import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext -val DatabaseCoroutineContext: CoroutineContext = newSingleThreadContext("db-context") +val DatabaseCoroutineContext: CoroutineContext = Dispatchers.IO diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt index 1ac58f237a5..6d7821d20ca 100644 --- a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt @@ -2,26 +2,68 @@ package dev.inmo.micro_utils.repos import android.database.sqlite.SQLiteDatabase import dev.inmo.micro_utils.coroutines.safely -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +private object ContextsPool { + private val contexts = mutableListOf() + private val mutex = Mutex(locked = false) + private val freeContexts = mutableListOf() + + suspend fun acquireContext(): CoroutineContext { + return mutex.withLock { + freeContexts.removeFirstOrNull() ?: Executors.newSingleThreadExecutor().asCoroutineDispatcher().also { + contexts.add(it) + } + } + } + + suspend fun freeContext(context: CoroutineContext) { + return mutex.withLock { + if (context in contexts && context !in freeContexts) { + freeContexts.add(context) + } + } + } + + suspend fun use(block: suspend (CoroutineContext) -> T): T = acquireContext().let { + try { + block(it) + } finally { + freeContext(it) + } + } +} + +class TransactionContext( + val databaseContext: CoroutineContext +): CoroutineContext.Element { + override val key: CoroutineContext.Key = TransactionContext + + companion object : CoroutineContext.Key +} suspend fun SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T { - return withContext(DatabaseCoroutineContext) { - when { - inTransaction() -> { - block() - } - else -> { - beginTransaction() - safely( - { - endTransaction() - throw it - } - ) { - block().also { - setTransactionSuccessful() - endTransaction() - } + return coroutineContext[TransactionContext] ?.let { + withContext(it.databaseContext) { + block() + } + } ?: ContextsPool.use { context -> + withContext(TransactionContext(context) + context) { + beginTransaction() + safely( + { + endTransaction() + throw it + } + ) { + block().also { + setTransactionSuccessful() + endTransaction() } } } diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt index 8de4676138a..16cd3d02d58 100644 --- a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt @@ -5,6 +5,7 @@ import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import dev.inmo.micro_utils.coroutines.safely +import dev.inmo.micro_utils.repos.versions.* import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -36,6 +37,9 @@ class StandardSQLHelper( override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {} } + val versionsRepo: VersionsRepo by lazy { + StandardVersionsRepo(AndroidSQLStandardVersionsRepoProxy(sqlOpenHelper)) + } suspend fun writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block) diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/versions/AndroidSQLStandardVersionsRepoProxy.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/versions/AndroidSQLStandardVersionsRepoProxy.kt new file mode 100644 index 00000000000..d52d9e9ddad --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/versions/AndroidSQLStandardVersionsRepoProxy.kt @@ -0,0 +1,63 @@ +package dev.inmo.micro_utils.repos.versions + +import android.database.sqlite.SQLiteOpenHelper +import androidx.core.content.contentValuesOf +import dev.inmo.micro_utils.repos.* +import kotlinx.coroutines.runBlocking + +/** + * Will create [VersionsRepo] based on [SQLiteOpenHelper] with table inside of [database] + */ +@Suppress("NOTHING_TO_INLINE") +inline fun versionsRepo(database: SQLiteOpenHelper): VersionsRepo = StandardVersionsRepo( + AndroidSQLStandardVersionsRepoProxy(database) +) + +class AndroidSQLStandardVersionsRepoProxy( + override val database: SQLiteOpenHelper +) : StandardVersionsRepoProxy { + private val tableName: String = "AndroidSQLStandardVersionsRepo" + private val tableNameColumnName = "tableName" + private val tableVersionColumnName = "version" + + init { + runBlocking(DatabaseCoroutineContext) { + database.writableTransaction { + createTable( + tableName, + tableNameColumnName to ColumnType.Text.NOT_NULLABLE, + tableVersionColumnName to ColumnType.Numeric.INTEGER() + ) + } + } + } + + override suspend fun getTableVersion(table: String): Int? = database.writableTransaction { + select( + tableName, + selection = "$tableNameColumnName=?", + selectionArgs = arrayOf(table), + limit = limitClause(1) + ).use { + if (it.moveToFirst()) { + it.getInt(tableVersionColumnName) + } else { + null + } + } + } + + override suspend fun updateTableVersion(table: String, version: Int) { + database.writableTransaction { + val updated = update( + tableName, + contentValuesOf(tableVersionColumnName to version), + "$tableNameColumnName=?", + arrayOf(table) + ) > 0 + if (!updated) { + insert(tableName, null, contentValuesOf(tableNameColumnName to table, tableVersionColumnName to version)) + } + } + } +} diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/versions/AndroidSharedPreferencesStandardVersionsRepoProxy.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/versions/AndroidSharedPreferencesStandardVersionsRepoProxy.kt new file mode 100644 index 00000000000..1e9f1e1c1a4 --- /dev/null +++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/versions/AndroidSharedPreferencesStandardVersionsRepoProxy.kt @@ -0,0 +1,49 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package dev.inmo.micro_utils.repos.versions + +import android.content.Context +import android.database.sqlite.SQLiteOpenHelper +import androidx.core.content.contentValuesOf +import dev.inmo.micro_utils.repos.* +import dev.inmo.micro_utils.repos.keyvalue.keyValueStore +import kotlinx.coroutines.runBlocking + +/** + * Will create [VersionsRepo] based on [T], but versions will be stored in [StandardKeyValueRepo] + * + * @receiver Will be used to create [KeyValueBasedVersionsRepoProxy] via [keyValueStore] and pass it to [StandardVersionsRepo] + * + * @see [KeyValueBasedVersionsRepoProxy] + * @see [keyValueStore] + */ +inline fun Context.versionsKeyValueRepo( + database: T +): VersionsRepo = StandardVersionsRepo( + KeyValueBasedVersionsRepoProxy( + keyValueStore("SPVersionsRepo"), + database + ) +) +/** + * Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [StandardKeyValueRepo] + * + * @receiver Will be used to create [StandardKeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo] + * + * @see [keyValueStore] + */ +inline fun Context.versionsKeyValueRepoForSQL( + database: SQLiteOpenHelper +) = versionsKeyValueRepo(database) + +/** + * Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [StandardKeyValueRepo] + * + * @param context Will be used to create [StandardKeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo] + * + * @see [keyValueStore] + */ +inline fun versionsRepo( + context: Context, + database: SQLiteOpenHelper +) = context.versionsKeyValueRepoForSQL(database) diff --git a/repos/exposed/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/exposed/versions/ExposedStandardVersionsRepoProxy.kt b/repos/exposed/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/exposed/versions/ExposedStandardVersionsRepoProxy.kt new file mode 100644 index 00000000000..a561949537e --- /dev/null +++ b/repos/exposed/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/exposed/versions/ExposedStandardVersionsRepoProxy.kt @@ -0,0 +1,48 @@ +package dev.inmo.micro_utils.repos.exposed.versions + +import dev.inmo.micro_utils.repos.exposed.ExposedRepo +import dev.inmo.micro_utils.repos.exposed.initTable +import dev.inmo.micro_utils.repos.versions.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + +/** + * Use this method to create [StandardVersionsRepo] based on [Database] with [ExposedStandardVersionsRepoProxy] as + * [StandardVersionsRepoProxy] + */ +@Suppress("NOTHING_TO_INLINE") +inline fun versionsRepo(database: Database): VersionsRepo = StandardVersionsRepo( + ExposedStandardVersionsRepoProxy(database) +) + +class ExposedStandardVersionsRepoProxy( + override val database: Database +) : StandardVersionsRepoProxy, Table("ExposedVersionsProxy"), ExposedRepo { + private val tableNameColumn = text("tableName") + private val tableVersionColumn = integer("tableName") + + init { + initTable() + } + + override suspend fun getTableVersion(tableName: String): Int? = transaction(database) { + select { tableNameColumn.eq(tableName) }.limit(1).firstOrNull() ?.getOrNull(tableVersionColumn) + } + + override suspend fun updateTableVersion(tableName: String, version: Int) { + transaction(database) { + val updated = update( + { tableNameColumn.eq(tableName) } + ) { + it[tableVersionColumn] = version + } > 0 + if (!updated) { + insert { + it[tableNameColumn] = tableName + it[tableVersionColumn] = version + } + } + } + } + +}