update dependencies

This commit is contained in:
2023-08-05 13:49:45 +06:00
parent ee948395e3
commit 4478193d8a
88 changed files with 24 additions and 23 deletions

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.repos.common"/>

View File

@@ -0,0 +1,46 @@
package dev.inmo.micro_utils.repos
sealed class ColumnType(
typeName: String,
nullable: Boolean
) {
open val asType: String = "$typeName${if (!nullable) " not null" else ""}"
sealed class Text(
nullable: Boolean
) : ColumnType("text", nullable) {
object NULLABLE : Text(true)
object NOT_NULLABLE : Text(false)
}
sealed class Numeric(
typeName: String,
autoincrement: Boolean = false,
primaryKey: Boolean = false,
nullable: Boolean = false
) : ColumnType(
typeName,
nullable
) {
override val asType: String = "${super.asType}${if (primaryKey) " primary key" else ""}${if (autoincrement) " autoincrement" else ""}"
class INTEGER(
autoincrement: Boolean = false,
primaryKey: Boolean = false,
nullable: Boolean = false
) : Numeric(
"integer",
autoincrement,
primaryKey,
nullable
)
class DOUBLE(autoincrement: Boolean = false, primaryKey: Boolean = false, nullable: Boolean = false) : Numeric(
"double",
autoincrement,
primaryKey,
nullable
)
}
override fun toString(): String {
return asType
}
}

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.repos
import androidx.core.content.contentValuesOf
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
fun contentValuesOfNotNull(vararg pairs: Pair<String, Any?>?) = contentValuesOf(
*(pairs.filter { it != null } as List<Pair<String, Any?>>).toTypedArray()
)

View File

@@ -0,0 +1,27 @@
package dev.inmo.micro_utils.repos
import android.database.Cursor
class CursorIterator(
private val c: Cursor
) : Iterator<Cursor> {
private var i = 0
init {
c.moveToFirst()
}
override fun hasNext(): Boolean {
return i < c.count
}
override fun next(): Cursor {
i++
return if (c.moveToNext()) {
c
} else {
throw NoSuchElementException()
}
}
}
operator fun Cursor.iterator(): CursorIterator = CursorIterator(this)

View File

@@ -0,0 +1,21 @@
package dev.inmo.micro_utils.repos
import android.database.Cursor
inline fun <T> Cursor.map(
block: (Cursor) -> T
): List<T> {
val result = mutableListOf<T>()
if (moveToFirst()) {
do {
result.add(block(this))
} while (moveToNext())
}
return result
}
fun Cursor.firstOrNull(): Cursor? = if (moveToFirst()) {
this
} else {
null
}

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.repos
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
val DatabaseCoroutineContext: CoroutineContext = Dispatchers.IO

View File

@@ -0,0 +1,135 @@
package dev.inmo.micro_utils.repos
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import androidx.core.database.*
import dev.inmo.micro_utils.repos.getLongOrNull
fun createTableQuery(
tableName: String,
vararg columnsToTypes: Pair<String, ColumnType>
) = "create table $tableName (${columnsToTypes.joinToString(", ") { "${it.first} ${it.second}" }});"
fun SQLiteDatabase.createTable(
tableName: String,
vararg columnsToTypes: Pair<String, ColumnType>,
onInit: (SQLiteDatabase.() -> Unit)? = null
): Boolean {
val existing = rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName'", null).use {
it.count > 0
}
return if (existing) {
false
// TODO:: add upgrade opportunity
} else {
execSQL(createTableQuery(tableName, *columnsToTypes))
onInit ?.invoke(this)
true
}
}
/**
* @throws IllegalArgumentException
*/
fun Cursor.getString(columnName: String): String = getString(
getColumnIndexOrThrow(columnName)
)
fun Cursor.getStringOrNull(columnName: String): String? {
return getStringOrNull(
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
)
}
/**
* @throws IllegalArgumentException
*/
fun Cursor.getShort(columnName: String): Short = getShort(
getColumnIndexOrThrow(columnName)
)
fun Cursor.getShortOrNull(columnName: String): Short? {
return getShortOrNull(
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
)
}
/**
* @throws IllegalArgumentException
*/
fun Cursor.getLong(columnName: String): Long = getLong(
getColumnIndexOrThrow(columnName)
)
fun Cursor.getLongOrNull(columnName: String): Long? {
return getLongOrNull(
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
)
}
/**
* @throws IllegalArgumentException
*/
fun Cursor.getInt(columnName: String): Int = getInt(
getColumnIndexOrThrow(columnName)
)
fun Cursor.getIntOrNull(columnName: String): Int? {
return getIntOrNull(
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
)
}
/**
* @throws IllegalArgumentException
*/
fun Cursor.getFloat(columnName: String): Float = getFloat(
getColumnIndexOrThrow(columnName)
)
fun Cursor.getFloatOrNull(columnName: String): Float? {
return getFloatOrNull(
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
)
}
/**
* @throws IllegalArgumentException
*/
fun Cursor.getDouble(columnName: String): Double = getDouble(
getColumnIndexOrThrow(columnName)
)
fun Cursor.getDoubleOrNull(columnName: String): Double? {
return getDoubleOrNull(
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
)
}
fun SQLiteDatabase.select(
table: String,
columns: Array<String>? = null,
selection: String? = null,
selectionArgs: Array<String>? = null,
groupBy: String? = null,
having: String? = null,
orderBy: String? = null,
limit: String? = null
) = query(
table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
)
fun SQLiteDatabase.selectDistinct(
table: String,
columns: Array<String>? = null,
selection: String? = null,
selectionArgs: Array<String>? = null,
groupBy: String? = null,
having: String? = null,
orderBy: String? = null,
limit: String? = null
) = query(
true, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
)
fun makePlaceholders(count: Int): String {
return (0 until count).joinToString { "?" }
}
fun makeStringPlaceholders(count: Int): String {
return (0 until count).joinToString { "\"?\"" }
}

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.repos
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.firstIndex
fun limitClause(size: Long, since: Long? = null) = "${since ?.let { "$it, " } ?: ""}$size"
fun limitClause(size: Int, since: Int? = null) = limitClause(size.toLong(), since ?.toLong())
fun Pagination.limitClause() = limitClause(size, firstIndex)

View File

@@ -0,0 +1,90 @@
package dev.inmo.micro_utils.repos
import android.database.sqlite.SQLiteDatabase
import dev.inmo.micro_utils.coroutines.safely
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<CoroutineContext>()
private val mutex = Mutex(locked = false)
private val freeContexts = mutableListOf<CoroutineContext>()
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 <T> use(block: suspend (CoroutineContext) -> T): T = acquireContext().let {
try {
safely {
block(it)
}
} finally {
freeContext(it)
}
}
}
class TransactionContext(
val databaseContext: CoroutineContext
): CoroutineContext.Element {
override val key: CoroutineContext.Key<TransactionContext> = TransactionContext
companion object : CoroutineContext.Key<TransactionContext>
}
suspend fun <T> SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T {
coroutineContext[TransactionContext] ?.let {
return withContext(it.databaseContext) {
block()
}
}
return ContextsPool.use { context ->
withContext(TransactionContext(context) + context) {
beginTransaction()
safely(
{
endTransaction()
throw it
}
) {
block().also {
setTransactionSuccessful()
endTransaction()
}
}
}
}
}
inline fun <T> SQLiteDatabase.inlineTransaction(crossinline block: SQLiteDatabase.() -> T): T {
return when {
inTransaction() -> block()
else -> {
beginTransaction()
try {
block().also { setTransactionSuccessful() }
} finally {
endTransaction()
}
}
}
}
fun <T> SQLiteDatabase.blockingTransaction(block: SQLiteDatabase.() -> T): T = inlineTransaction(block)

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.repos
val internalId = "_id"
val internalIdType = ColumnType.Numeric.INTEGER(
autoincrement = true,
primaryKey = true
)

View File

@@ -0,0 +1,65 @@
package dev.inmo.micro_utils.repos
import android.content.Context
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.keyvalue.keyValueStore
import dev.inmo.micro_utils.repos.versions.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class StandardSQLHelper(
context: Context,
name: String,
factory: SQLiteDatabase.CursorFactory? = null,
version: Int = 1,
errorHandler: DatabaseErrorHandler? = null,
useSharedPreferencesForVersions: Boolean = false
) {
val sqlOpenHelper = object : SQLiteOpenHelper(context, name, factory, version, errorHandler) {
override fun onCreate(db: SQLiteDatabase?) {}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
}
val versionsRepo: VersionsRepo<SQLiteOpenHelper> by lazy {
StandardVersionsRepo(
if (useSharedPreferencesForVersions) {
KeyValueBasedVersionsRepoProxy(
context.keyValueStore("AndroidSPStandardVersionsRepo"),
sqlOpenHelper
)
} else {
AndroidSQLStandardVersionsRepoProxy(sqlOpenHelper)
}
)
}
suspend fun <T> writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
suspend fun <T> readableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.readableTransaction(block)
}
fun <T> SQLiteOpenHelper.blockingWritableTransaction(block: SQLiteDatabase.() -> T): T {
return writableDatabase.blockingTransaction(block)
}
fun <T> SQLiteOpenHelper.blockingReadableTransaction(block: SQLiteDatabase.() -> T): T {
return readableDatabase.blockingTransaction(block)
}
fun <T> StandardSQLHelper.blockingWritableTransaction(block: SQLiteDatabase.() -> T): T {
return sqlOpenHelper.blockingWritableTransaction(block)
}
fun <T> StandardSQLHelper.blockingReadableTransaction(block: SQLiteDatabase.() -> T): T {
return sqlOpenHelper.blockingReadableTransaction(block)
}
suspend fun <T> SQLiteOpenHelper.writableTransaction(block: suspend SQLiteDatabase.() -> T): T {
return writableDatabase.transaction(block)
}
suspend fun <T> SQLiteOpenHelper.readableTransaction(block: suspend SQLiteDatabase.() -> T): T {
return readableDatabase.transaction(block)
}

View File

@@ -0,0 +1,99 @@
package dev.inmo.micro_utils.repos.crud
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.createPaginationResult
import dev.inmo.micro_utils.repos.*
val <T> T.asId: String
get() = (this as? String) ?: this!!.toString()
abstract class AbstractAndroidCRUDRepo<ObjectType, IdType>(
protected val helper: StandardSQLHelper
) : ReadCRUDRepo<ObjectType, IdType> {
protected abstract val tableName: String
protected abstract val idColumnName: String
protected abstract suspend fun Cursor.toObject(): ObjectType
protected abstract suspend fun Cursor.toId(): IdType
protected fun SQLiteDatabase.count(): Long = select(tableName).use {
it.count
}.toLong()
override suspend fun contains(id: IdType): Boolean = helper.blockingReadableTransaction {
select(
tableName,
null,
"$idColumnName=?",
arrayOf(id.asId)
).use {
it.count > 0
}
}
override suspend fun getAll(): Map<IdType, ObjectType> = helper.readableTransaction {
select(
tableName,
null,
""
).use {
it.map {
it.toId() to it.toObject()
}
}
}.toMap()
override suspend fun getById(id: IdType): ObjectType? = helper.readableTransaction {
select(
tableName,
selection = "$idColumnName=?",
selectionArgs = arrayOf(id.asId),
limit = limitClause(1)
).use { c ->
if (c.moveToFirst()) {
c.toObject()
} else {
null
}
}
}
override suspend fun getByPagination(pagination: Pagination): PaginationResult<ObjectType> {
return helper.readableTransaction {
select(
tableName,
limit = pagination.limitClause()
).use {
if (it.moveToFirst()) {
val resultList = mutableListOf(it.toObject())
while (it.moveToNext()) {
resultList.add(it.toObject())
}
resultList.createPaginationResult(pagination, count())
} else {
emptyList<ObjectType>().createPaginationResult(pagination, 0)
}
}
}
}
override suspend fun getIdsByPagination(pagination: Pagination): PaginationResult<IdType> {
return helper.readableTransaction {
select(
tableName,
limit = pagination.limitClause()
).use {
if (it.moveToFirst()) {
val resultList = mutableListOf(it.toId())
while (it.moveToNext()) {
resultList.add(it.toId())
}
resultList.createPaginationResult(pagination, count())
} else {
emptyList<IdType>().createPaginationResult(pagination, 0)
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
package dev.inmo.micro_utils.repos.crud
import android.content.ContentValues
import dev.inmo.micro_utils.common.mapNotNullA
import dev.inmo.micro_utils.repos.*
import kotlinx.coroutines.flow.*
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
helper: StandardSQLHelper,
replyInFlows: Int = 0,
extraBufferCapacityInFlows: Int = 64
) : WriteCRUDRepo<ObjectType, IdType, InputValueType>,
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper),
CRUDRepo<ObjectType, IdType, InputValueType> {
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyInFlows, extraBufferCapacityInFlows)
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
override suspend fun create(values: List<InputValueType>): List<ObjectType> {
val valuesContentValues = values.map { it.asContentValues() }
val indexes = helper.blockingWritableTransaction {
valuesContentValues.map {
insert(tableName, null, it)
}
}
return helper.readableTransaction {
indexes.mapNotNullA { i ->
select(
tableName,
selection = "$internalId=?",
selectionArgs = arrayOf(i.toString())
).use { c ->
if (c.moveToFirst()) {
c.toObject()
} else {
null
}
}
}
}.also {
it.forEach {
newObjectsChannel.emit(it)
}
}
}
override suspend fun deleteById(ids: List<IdType>) {
val deleted = mutableListOf<IdType>()
helper.blockingWritableTransaction {
for (id in ids) {
delete(tableName, "$idColumnName=?", arrayOf(id.asId)).also {
if (it > 0) {
deleted.add(id)
}
}
}
}
for (deletedItem in deleted) {
deleteObjectsIdsChannel.emit(deletedItem)
}
}
override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
val asContentValues = value.asContentValues(id)
if (asContentValues.keySet().isNotEmpty()) {
helper.blockingWritableTransaction {
update(
tableName,
asContentValues,
"$idColumnName=?",
arrayOf(id.asId)
)
}
}
return getById(id) ?.also {
updateObjectsChannel.emit(it)
}
}
override suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType> {
val contentValues = values.map { (id, value) -> id to value.asContentValues(id) }
helper.writableTransaction {
contentValues.forEach { (id, contentValues) ->
update(
tableName,
contentValues,
"$idColumnName=?",
arrayOf(id.asId)
)
}
}
return values.mapNotNullA {
getById(it.first)
}.also {
it.forEach {
updateObjectsChannel.emit(it)
}
}
}
override suspend fun count(): Long = helper.blockingReadableTransaction { select(tableName).use { it.count.toLong() } }
}

View File

@@ -0,0 +1,196 @@
package dev.inmo.micro_utils.repos.keyvalue
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.pagination.utils.reverse
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.pagination.maxPagePagination
import kotlinx.coroutines.flow.*
private val cache = HashMap<String, KeyValueStore<*>>()
fun <T : Any> Context.keyValueStore(
name: String = "default",
cacheValues: Boolean = false
): KeyValueRepo<String, T> {
@Suppress("UNCHECKED_CAST")
return cache.getOrPut(name) {
KeyValueStore<T>(c = this, preferencesName = name, useCache = cacheValues)
} as KeyValueStore<T>
}
class KeyValueStore<T : Any> internal constructor (
c: Context,
preferencesName: String,
useCache: Boolean = false
) : SharedPreferences.OnSharedPreferenceChangeListener, KeyValueRepo<String, T> {
private val sharedPreferences = c.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
private val cachedData = if (useCache) {
mutableMapOf<String, Any>()
} else {
null
}
private val onNewValueChannel = MutableSharedFlow<Pair<String, T>>()
private val _onValueRemovedFlow = MutableSharedFlow<String>()
override val onNewValue: Flow<Pair<String, T>> = onNewValueChannel.asSharedFlow()
override val onValueRemoved: Flow<String> = _onValueRemovedFlow.asSharedFlow()
init {
cachedData ?.let {
for ((key, value) in sharedPreferences.all) {
if (value != null) {
cachedData[key] = value
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
}
override fun onSharedPreferenceChanged(sp: SharedPreferences, key: String) {
val value = sp.all[key]
cachedData ?: return
if (value != null) {
cachedData[key] = value
} else {
cachedData.remove(key)
}
}
override suspend fun get(k: String): T? {
@Suppress("UNCHECKED_CAST")
return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T
}
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<T> {
val resultPagination = if (reversed) pagination.reverse(count()) else pagination
return sharedPreferences.all.values.paginate(
resultPagination
).let {
it.changeResultsUnchecked(
it.results.map {
@Suppress("UNCHECKED_CAST")
it as T
}.let { if (reversed) it.reversed() else it }
)
}
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> {
val resultPagination = if (reversed) pagination.reverse(count()) else pagination
return sharedPreferences.all.keys.paginate(
resultPagination
).let {
it.changeResultsUnchecked(
it.results.let { if (reversed) it.reversed() else it }
)
}
}
override suspend fun keys(v: T, pagination: Pagination, reversed: Boolean): PaginationResult<String> {
val resultPagination = if (reversed) pagination.reverse(count()) else pagination
return sharedPreferences.all.mapNotNull { (k, value) -> if (value == v) k else null }.paginate(
resultPagination
).let {
it.changeResultsUnchecked(
it.results.let { if (reversed) it.reversed() else it }
)
}
}
override suspend fun contains(key: String): Boolean = sharedPreferences.contains(key)
override suspend fun getAll(): Map<String, T> {
val resultMap = mutableMapOf<String, T>()
for ((k, v) in sharedPreferences.all) {
@Suppress("UNCHECKED_CAST")
resultMap[k] = (v as? T) ?: continue
}
return resultMap.toMap()
}
override suspend fun count(): Long = sharedPreferences.all.size.toLong()
override suspend fun set(toSet: Map<String, T>) {
sharedPreferences.edit {
for ((k, v) in toSet) {
when(v) {
is Int -> putInt(k, v)
is Long -> putLong(k, v)
is Float -> putFloat(k, v)
is String -> putString(k, v)
is Boolean -> putBoolean(k, v)
is Set<*> -> putStringSet(k, v.map { (it as? String) ?: it.toString() }.toSet())
else -> error(
"Currently supported only primitive types and set for SharedPreferences KeyValue repos"
)
}
}
}
for ((k, v) in toSet) {
onNewValueChannel.emit(k to v)
}
}
override suspend fun unset(toUnset: List<String>) {
sharedPreferences.edit {
for (item in toUnset) {
remove(item)
}
}
for (it in toUnset) {
_onValueRemovedFlow.emit(it)
}
}
override suspend fun unsetWithValues(toUnset: List<T>) {
val keysToRemove = sharedPreferences.all.mapNotNull { if (it.value in toUnset) it.key else null }
sharedPreferences.edit {
keysToRemove.map {
remove(it)
}
}
keysToRemove.forEach {
_onValueRemovedFlow.emit(it)
}
}
override suspend fun clear() {
val keys = mutableSetOf<String>()
doWithPagination(maxPagePagination()) {
keys(it).also {
keys.addAll(it.results)
}.nextPageIfNotEmpty()
}
val success = sharedPreferences.edit().apply {
clear()
}.commit()
if (success) {
keys.forEach {
_onValueRemovedFlow.emit(it)
}
}
}
companion object {
operator fun <T : Any> invoke(
context: Context,
name: String = "default",
cacheValues: Boolean = false
) = context.keyValueStore<T>(name, cacheValues)
}
}
inline fun <T : Any> SharedPreferencesKeyValueRepo(
context: Context,
name: String = "default",
cacheValues: Boolean = false
) = context.keyValueStore<T>(name, cacheValues)
typealias KeyValueSPRepo<T> = KeyValueStore<T>

View File

@@ -0,0 +1,298 @@
package dev.inmo.micro_utils.repos.onetomany
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.content.contentValuesOf
import dev.inmo.micro_utils.common.mapNotNullA
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.reverse
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.crud.asId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
private val internalSerialFormat = Json {
ignoreUnknownKeys = true
}
typealias KeyValuesAndroidRepo<Key, Value> = OneToManyAndroidRepo<Key, Value>
class OneToManyAndroidRepo<Key, Value>(
private val tableName: String,
private val keyAsString: Key.() -> String,
private val valueAsString: Value.() -> String,
private val keyFromString: String.() -> Key,
private val valueFromString: String.() -> Value,
private val helper: SQLiteOpenHelper
) : KeyValuesRepo<Key, Value> {
private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
private val _onValueRemoved: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
override val onValueRemoved: Flow<Pair<Key, Value>> = _onValueRemoved.asSharedFlow()
private val _onDataCleared = MutableSharedFlow<Key>()
override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
private val idColumnName = "id"
private val idColumnArray = arrayOf(idColumnName)
private val valueColumnName = "value"
private val valueColumnArray = arrayOf(valueColumnName)
init {
helper.blockingWritableTransaction {
createTable(
tableName,
internalId to internalIdType,
idColumnName to ColumnType.Text.NOT_NULLABLE,
valueColumnName to ColumnType.Text.NULLABLE
)
}
}
override suspend fun add(toAdd: Map<Key, List<Value>>) {
val added = mutableListOf<Pair<Key, Value>>()
helper.blockingWritableTransaction {
for ((k, values) in toAdd) {
values.forEach { v ->
val kAsString = k.keyAsString()
val vAsString = v.valueAsString()
val isThere = select(tableName,
null,
"$idColumnName=? AND $valueColumnName=?",
arrayOf(kAsString, vAsString),
limit = limitClause(1)
).use { it.moveToFirst() }
if (isThere) {
return@forEach
}
insert(
tableName,
null,
contentValuesOf(
idColumnName to k.keyAsString(),
valueColumnName to v.valueAsString()
)
).also {
if (it != -1L) {
added.add(k to v)
}
}
}
}
}
added.forEach { _onNewValue.emit(it) }
}
override suspend fun clear(k: Key) {
helper.blockingWritableTransaction {
delete(tableName, "$idColumnName=?", arrayOf(k.keyAsString()))
}.also {
if (it > 0) {
_onDataCleared.emit(k)
}
}
}
override suspend fun set(toSet: Map<Key, List<Value>>) {
val (clearedKeys, inserted) = helper.blockingWritableTransaction {
toSet.mapNotNull { (k, _) ->
if (delete(tableName, "$idColumnName=?", arrayOf(k.keyAsString())) > 0) {
k
} else {
null
}
} to toSet.flatMap { (k, values) ->
values.map { v ->
insert(
tableName,
null,
contentValuesOf(idColumnName to k.keyAsString(), valueColumnName to v.valueAsString())
)
k to v
}
}
}
clearedKeys.forEach { _onDataCleared.emit(it) }
inserted.forEach { newPair -> _onNewValue.emit(newPair) }
}
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = firstPageWithOneElementPagination.limitClause()).use {
it.count > 0
}
}
override suspend fun contains(k: Key, v: Value): Boolean = helper.blockingReadableTransaction {
select(
tableName,
selection = "$idColumnName=? AND $valueColumnName=?",
selectionArgs = arrayOf(k.keyAsString(), v.valueAsString()),
limit = FirstPagePagination(1).limitClause()
).use {
it.count > 0
}
}
override suspend fun count(): Long = helper.blockingReadableTransaction {
select(
tableName
).use {
it.count
}
}.toLong()
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
selectDistinct(
tableName,
columns = valueColumnArray,
selection = "$idColumnName=?",
selectionArgs = arrayOf(k.keyAsString())
).use {
it.count
}
}.toLong()
override suspend fun get(
k: Key,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Value> = count(k).let { count ->
if (pagination.firstIndex >= count) {
return@let emptyList<Value>().createPaginationResult(
pagination,
count
)
}
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
helper.blockingReadableTransaction {
select(
tableName,
valueColumnArray,
selection = "$idColumnName=?",
selectionArgs = arrayOf(k.keyAsString()),
limit = resultPagination.limitClause()
).use { c ->
mutableListOf<Value>().also {
if (c.moveToFirst()) {
do {
it.add(c.getString(valueColumnName).valueFromString())
} while (c.moveToNext())
}
}
}
}.createPaginationResult(
pagination,
count
)
}
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<Key> = count().let { count ->
if (pagination.firstIndex >= count) {
return@let emptyList<Key>().createPaginationResult(
pagination,
count
)
}
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
helper.blockingReadableTransaction {
selectDistinct(
tableName,
idColumnArray,
limit = resultPagination.limitClause()
).use { c ->
mutableListOf<Key>().also {
if (c.moveToFirst()) {
do {
it.add(c.getString(idColumnName).keyFromString())
} while (c.moveToNext())
}
}
}
}.createPaginationResult(
pagination,
count
)
}
override suspend fun keys(
v: Value,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Key> = count().let { count ->
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
helper.blockingReadableTransaction {
selectDistinct(
tableName,
idColumnArray,
selection = "$valueColumnName=?",
selectionArgs = arrayOf(v.valueAsString()),
limit = resultPagination.limitClause()
).use { c ->
mutableListOf<Key>().also {
if (c.moveToFirst()) {
do {
it.add(c.getString(idColumnName).keyFromString())
} while (c.moveToNext())
}
}
}
}.createPaginationResult(
pagination,
count
)
}
override suspend fun remove(toRemove: Map<Key, List<Value>>) {
helper.blockingWritableTransaction {
toRemove.flatMap { (k, vs) ->
vs.mapNotNullA { v ->
if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.keyAsString(), v.valueAsString())) > 0) {
k to v
} else {
null
}
}
}
}.forEach { (k, v) ->
_onValueRemoved.emit(k to v)
}
}
override suspend fun removeWithValue(v: Value) {
helper.blockingWritableTransaction {
val keys = select(tableName, idColumnArray, "$valueColumnName=?", arrayOf(v.valueAsString())).map {
it.asId.keyFromString()
}
keys.filter {
delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(it.keyAsString(), v.valueAsString())) > 0
}
}.forEach { k ->
_onValueRemoved.emit(k to v)
}
}
}
fun <Key, Value> OneToManyAndroidRepo(
tableName: String,
keySerializer: KSerializer<Key>,
valueSerializer: KSerializer<Value>,
helper: SQLiteOpenHelper
) = OneToManyAndroidRepo(
tableName,
{ internalSerialFormat.encodeToString(keySerializer, this) },
{ internalSerialFormat.encodeToString(valueSerializer, this) },
{ internalSerialFormat.decodeFromString(keySerializer, this) },
{ internalSerialFormat.decodeFromString(valueSerializer, this) },
helper
)
fun <Key, Value> KeyValuesAndroidRepo(
tableName: String,
keySerializer: KSerializer<Key>,
valueSerializer: KSerializer<Value>,
helper: SQLiteOpenHelper
) = OneToManyAndroidRepo(tableName, keySerializer, valueSerializer, helper)

View File

@@ -0,0 +1,61 @@
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<SQLiteOpenHelper> = StandardVersionsRepo(
AndroidSQLStandardVersionsRepoProxy(database)
)
class AndroidSQLStandardVersionsRepoProxy(
override val database: SQLiteOpenHelper
) : StandardVersionsRepoProxy<SQLiteOpenHelper> {
private val tableName: String = "AndroidSQLStandardVersionsRepo"
private val tableNameColumnName = "tableName"
private val tableVersionColumnName = "version"
init {
database.blockingWritableTransaction {
createTable(
tableName,
tableNameColumnName to ColumnType.Text.NOT_NULLABLE,
tableVersionColumnName to ColumnType.Numeric.INTEGER()
)
}
}
override suspend fun getTableVersion(tableName: String): Int? = database.writableTransaction {
select(
this@AndroidSQLStandardVersionsRepoProxy.tableName,
selection = "$tableNameColumnName=?",
selectionArgs = arrayOf(tableName),
limit = limitClause(1)
).use {
if (it.moveToFirst()) {
it.getInt(tableVersionColumnName)
} else {
null
}
}
}
override suspend fun updateTableVersion(tableName: String, version: Int) {
database.writableTransaction {
val updated = update(
this@AndroidSQLStandardVersionsRepoProxy.tableName,
contentValuesOf(tableVersionColumnName to version),
"$tableNameColumnName=?",
arrayOf(tableName)
) > 0
if (!updated) {
insert(this@AndroidSQLStandardVersionsRepoProxy.tableName, null, contentValuesOf(tableNameColumnName to tableName, tableVersionColumnName to version))
}
}
}
}

View File

@@ -0,0 +1,47 @@
@file:Suppress("NOTHING_TO_INLINE")
package dev.inmo.micro_utils.repos.versions
import android.content.Context
import android.database.sqlite.SQLiteOpenHelper
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.keyvalue.keyValueStore
/**
* Will create [VersionsRepo] based on [T], but versions will be stored in [KeyValueRepo]
*
* @receiver Will be used to create [KeyValueBasedVersionsRepoProxy] via [keyValueStore] and pass it to [StandardVersionsRepo]
*
* @see [KeyValueBasedVersionsRepoProxy]
* @see [keyValueStore]
*/
inline fun <T> Context.versionsKeyValueRepo(
database: T
): VersionsRepo<T> = StandardVersionsRepo(
KeyValueBasedVersionsRepoProxy(
keyValueStore("SPVersionsRepo"),
database
)
)
/**
* Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [KeyValueRepo]
*
* @receiver Will be used to create [KeyValueRepo] 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 [KeyValueRepo]
*
* @param context Will be used to create [KeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo]
*
* @see [keyValueStore]
*/
inline fun versionsRepo(
context: Context,
database: SQLiteOpenHelper
) = context.versionsKeyValueRepoForSQL(database)