Compare commits

..

28 Commits

Author SHA1 Message Date
3ae2c84c08 fill changelog 2026-02-15 18:38:07 +06:00
b2e6ab51cb update runCatchingLogging to rethrow CanellationException 2026-02-15 18:35:43 +06:00
d6496f02e6 start 0.28.1 2026-02-15 18:21:57 +06:00
38e98327b4 fill changelog for 0.28.0 2026-02-04 21:26:32 +06:00
c72a6fda5d Merge pull request #638 from InsanusMokrassar/0.28.0
0.28.0
2026-01-27 23:37:56 +06:00
23d58d42ee make it buildable 2026-01-27 16:19:43 +06:00
d972cebf47 start update dependencies 2026-01-24 19:41:35 +06:00
3558a5135b start 0.28.0 2026-01-24 16:12:54 +06:00
82460d2ce3 Merge pull request #633 from InsanusMokrassar/0.27.0
0.27.0
2026-01-15 12:53:41 +06:00
4ce2b7d3b2 update compose versions 2026-01-15 12:07:53 +06:00
2c10bb3c88 update dependencies
io.ktor:ktor-client-core 3.3.2 -> 3.3.3

com.squareup.okio:okio 3.16.2 -> 3.16.4

com.google.devtools.ksp:symbol-processing-api 2.3.2 -> 2.3.4
2026-01-15 11:56:03 +06:00
01bed4a6c9 start 0.27.0 2026-01-15 11:34:35 +06:00
52c4867468 Merge pull request #624 from InsanusMokrassar/0.26.8
0.26.8
2025-11-12 22:47:29 +06:00
92b3fd25e7 fill changelog 2025-11-12 22:25:33 +06:00
e0e1da5082 add suspendPoint 2025-11-12 22:20:17 +06:00
80953f5d09 update ktor and android script config 2025-11-12 18:18:01 +06:00
2849db57f2 start 0.26.8 2025-11-12 18:01:11 +06:00
0170b92272 Merge pull request #622 from InsanusMokrassar/0.26.7
0.26.7
2025-11-05 15:41:10 +06:00
7bcb81400b fill changelog 2025-11-05 15:14:21 +06:00
078aedfb68 update dependencies 2025-11-05 13:42:27 +06:00
cb56bf9793 Revert "add getCurrentLocale and compose translation"
This reverts commit fce47897d5.
2025-11-05 12:29:33 +06:00
b152986b4e improve SmartKeyRWLockerTests and fix SmartSemaphore 2025-11-05 12:27:24 +06:00
fce47897d5 add getCurrentLocale and compose translation 2025-10-31 19:34:09 +06:00
0b701a3e99 start 0.26.7 2025-10-31 11:21:06 +06:00
9dad353957 Merge pull request #619 from InsanusMokrassar/0.26.6
0.26.6
2025-10-19 16:24:37 +06:00
89e16b7bdb update dependencies
io.ktor:ktor-* 3.3.0 -> 3.3.1
com.squareup.okio:okio 3.16.0 -> 3.16.2
org.jetbrains.dokka:dokka-gradle-plugin 2.0.0 -> 2.1.0
2025-10-19 16:23:23 +06:00
c2965da341 start 0.26.6 2025-10-19 15:24:41 +06:00
ffb072dc5f Merge pull request #613 from InsanusMokrassar/0.26.5
0.26.5
2025-10-04 14:39:30 +06:00
43 changed files with 828 additions and 268 deletions

View File

@@ -1,5 +1,72 @@
# Changelog
## 0.28.1
* `Coroutines`:
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions
## 0.28.0
**THIS VERSION CONTAINS BREAKING CHANGES DUE TO EXPOSED 1.0.0 UPDATE**
* `Versions`:
* `Kotlin`: `2.2.21` -> `2.3.0`
* `Serialization`: `1.9.0` -> `1.10.0`
* `Exposed`: `0.61.0` -> `1.0.0` (**MAJOR VERSION UPDATE**)
* `Ktor`: `3.3.3` -> `3.4.0`
* `NMCP`: `1.2.0` -> `1.2.1`
* `Repos`:
* `Exposed`:
* All Exposed-based repositories have been updated to support Exposed 1.0.0 API changes
* Import paths have been migrated to new `org.jetbrains.exposed.v1.*` package structure
* `Pagination`:
* `Exposed`:
* Updated to use new Exposed 1.0.0 import paths
## 0.27.0
* `Versions`:
* `Ktor`: `3.3.2` -> `3.3.3`
* `Okio`: `3.16.2` -> `3.16.4`
* `KSP`: `2.3.2` -> `2.3.4`
* `Compose`: `1.9.3` -> `1.10.0`
* `Compose Material3`: `1.9.0` -> `1.10.0-alpha05`
## 0.26.8
* `Versions`:
* `KSLog`: `1.5.1` -> `1.5.2`
* `Compose`: `1.9.2` -> `1.9.3`
* `Ktor`: `3.3.1` -> `3.3.2`
* `Coroutines`:
* Add simple suspend function `suspendPoint` which will ensure that current coroutine is active to let it be
destroyable even in case it have non-suspendable nature
## 0.26.7
* `Versions`:
* `Kotlin`: `2.2.20` -> `2.2.21`
* `Compose`: `1.8.2` -> `1.9.2`
* `KSP`: `2.2.20-2.0.3` -> `2.3.1`
* `Coroutines`:
* Fix `SmartSemaphore.waitRelease` to wait for the exact number of permits
* Improve `SmartKeyRWLocker` tests
* `KSP`:
* `Sealed`/`ClassCasts`/`Variations`:
* Add workaround for `NoSuchElementException` to improve processors stability on new `KSP`
* `Koin`:
* `Generator`:
* Handle missing annotation values safely (`NoSuchElementException` workaround)
* `Android`:
* `Pickers`:
* Add dependency `androidx.compose.material:material-icons-extended`
## 0.26.6
* `Versions`:
* `Ktor`: `3.3.0` -> `3.3.1`
* `Okio`: `3.16.0` -> `3.16.2`
## 0.26.5
* `Versions`:

View File

@@ -13,6 +13,7 @@ kotlin {
androidMain {
dependencies {
api project(":micro_utils.android.smalltextfield")
api libs.jb.compose.icons
}
}
}

View File

@@ -44,6 +44,11 @@ allprojects {
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
}
it.tasks.withType(AbstractTestTask).configureEach {
it.failOnNoDiscoveredTests = false
}
}
apply from: "./extensions.gradle"

View File

@@ -2,11 +2,27 @@ package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e
import kotlin.coroutines.cancellation.CancellationException
/**
* Executes the given [block] within a `runCatching` context and logs any exceptions that occur, excluding
* `CancellationException` which is rethrown. This method simplifies error handling by automatically logging
* the errors using the provided [logger].
*
* @param T The result type of the [block].
* @param R The receiver type on which this function operates.
* @param errorMessageBuilder A lambda to build the error log message. By default, it returns a generic error message.
* @param logger The logging instance used for logging errors. Defaults to [KSLog].
* @param block The code block to execute within the `runCatching` context.
* @return A [Result] representing the outcome of executing the [block].
*/
inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
block: R.() -> T
) = runCatching(block).onFailure {
logger.e(it) { errorMessageBuilder(it) }
when (it) {
is CancellationException -> throw it
else -> logger.e(it) { errorMessageBuilder(it) }
}
}

View File

@@ -72,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits
}
if (shouldContinue) {
waitRelease()
waitRelease(checkedPermits - acquiredPermits)
}
} while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) {

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
/**
* Ensures that the current coroutine context is still active and throws a [kotlinx.coroutines.CancellationException]
* if the coroutine has been canceled.
*
* This function provides a convenient way to check the active status of a coroutine, which is useful
* to identify cancellation points in long-running or suspendable operations.
*
* @throws kotlinx.coroutines.CancellationException if the coroutine context is no longer active.
*/
suspend fun suspendPoint() = currentCoroutineContext().ensureActive()

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
@@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests {
@Test
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite()
private lateinit var locker: SmartKeyRWLocker<String>
assertTrue { locker.isWriteLocked() }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.unlockWrite()
assertFalse { locker.isWriteLocked() }
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
@BeforeTest
fun setup() {
locker = SmartKeyRWLocker()
}
// ==================== Global Read Tests ====================
@Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val results = mutableListOf<Boolean>()
locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits)
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
val jobs = List(5) {
launch {
locker.acquireRead()
delay(100.milliseconds)
results.add(true)
locker.releaseRead()
}
}
assertFalse { locker.isWriteLocked(testKey) }
jobs.joinAll()
locker.releaseRead()
assertEquals(5, results.size)
}
@Test
fun testGlobalReadBlocksGlobalWrite() = runTest {
locker.acquireRead()
var writeAcquired = false
val writeJob = launch {
locker.lockWrite()
writeAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write should be blocked by global read")
locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits)
writeJob.join()
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
assertTrue(writeAcquired, "Write should succeed after read released")
}
@Test
fun readLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
fun testGlobalReadBlocksAllKeyWrites() = runTest {
locker.acquireRead()
assertTrue { locker.isWriteLocked(testKey) }
val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
assertFails {
realWithTimeout(1.seconds) {
val jobs = keys.map { key ->
launch {
locker.lockWrite(key)
writeFlags[key] = true
locker.unlockWrite(key)
}
}
delay(200.milliseconds)
assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active")
locker.releaseRead()
jobs.joinAll()
assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released")
}
// ==================== Global Write Tests ====================
@Test
fun testGlobalWriteBlocksAllOperations() = runTest {
locker.lockWrite()
var globalReadAcquired = false
var keyReadAcquired = false
var keyWriteAcquired = false
val jobs = listOf(
launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
}
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
)
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked")
assertFalse(keyReadAcquired, "Key read should be blocked")
assertFalse(keyWriteAcquired, "Key write should be blocked")
realWithTimeout(1.seconds) {
locker.acquireRead()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
assertTrue { locker.releaseRead() }
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits)
locker.unlockWrite()
jobs.joinAll()
assertTrue(globalReadAcquired)
assertTrue(keyReadAcquired)
assertTrue(keyWriteAcquired)
}
@Test
fun writeLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
fun testGlobalWriteIsExclusive() = runTest {
locker.lockWrite()
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
var secondWriteAcquired = false
val job = launch {
locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
}
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() }
assertFalse(locker.isWriteLocked())
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
}
// ==================== Key Read Tests ====================
@Test
fun readsBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val key = "testKey"
val results = mutableListOf<Boolean>()
val testKeys = (0 until 100).map { "test$it" }
locker.acquireRead(key)
for (i in testKeys.indices) {
val it = testKeys[i]
locker.acquireRead(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
val jobs = List(5) {
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results.add(true)
locker.releaseRead(key)
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() }
jobs.joinAll()
locker.releaseRead(key)
assertEquals(5, results.size)
}
@Test
fun testKeyReadAllowsReadsForDifferentKeys() = runTest {
val results = mutableMapOf<String, Boolean>()
locker.acquireRead("key1")
val jobs = listOf("key2", "key3", "key4").map { key ->
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results[key] = true
locker.releaseRead(key)
}
val readPermitsBeforeLock = locker.readSemaphore().freePermits
realWithTimeout(1.seconds) { locker.acquireRead() }
}
jobs.joinAll()
locker.releaseRead("key1")
assertEquals(3, results.size)
}
@Test
fun testKeyReadBlocksWriteForSameKey() = runTest {
val key = "testKey"
locker.acquireRead(key)
var writeAcquired = false
val job = launch {
locker.lockWrite(key)
writeAcquired = true
locker.unlockWrite(key)
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write for same key should be blocked")
locker.releaseRead(key)
job.join()
assertTrue(writeAcquired)
}
@Test
fun testKeyReadBlocksGlobalWrite() = runTest {
locker.acquireRead("key1")
var globalWriteAcquired = false
val job = launch {
locker.lockWrite()
globalWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(globalWriteAcquired, "Global write should be blocked by key read")
locker.releaseRead("key1")
job.join()
assertTrue(globalWriteAcquired)
}
@Test
fun testKeyReadAllowsWriteForDifferentKey() = runTest {
locker.acquireRead("key1")
var writeAcquired = false
val job = launch {
locker.lockWrite("key2")
writeAcquired = true
locker.unlockWrite("key2")
}
job.join()
assertTrue(writeAcquired, "Write for different key should succeed")
locker.releaseRead("key1")
}
// ==================== Key Write Tests ====================
@Test
fun testKeyWriteBlocksReadForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
var readAcquired = false
val job = launch {
locker.acquireRead(key)
readAcquired = true
locker.releaseRead(key)
}
delay(200.milliseconds)
assertFalse(readAcquired, "Read for same key should be blocked")
locker.unlockWrite(key)
job.join()
assertTrue(readAcquired)
}
@Test
fun testKeyWriteBlocksGlobalRead() = runTest {
locker.lockWrite("key1")
var globalReadAcquired = false
val job = launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked by key write")
locker.unlockWrite("key1")
job.join()
assertTrue(globalReadAcquired)
}
@Test
fun writesBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
fun testKeyWriteIsExclusiveForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
val testKeys = (0 until 100).map { "test$it" }
var secondWriteAcquired = false
val job = launch {
locker.lockWrite(key)
secondWriteAcquired = true
locker.unlockWrite(key)
}
for (i in testKeys.indices) {
val it = testKeys[i]
locker.lockWrite(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
previous.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true }
locker.unlockWrite(key)
job.join()
assertTrue(secondWriteAcquired)
}
@Test
fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest {
locker.lockWrite("key1")
val results = mutableMapOf<String, Boolean>()
val jobs = listOf(
launch {
locker.acquireRead("key2")
results["read-key2"] = true
locker.releaseRead("key2")
},
launch {
locker.lockWrite("key3")
results["write-key3"] = true
locker.unlockWrite("key3")
}
next.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
)
jobs.joinAll()
assertEquals(2, results.size, "Operations on different keys should succeed")
locker.unlockWrite("key1")
}
// ==================== Complex Scenarios ====================
@Test
fun testMultipleReadersThenWriter() = runTest {
val key = "testKey"
val readCount = 5
val readers = mutableListOf<Job>()
repeat(readCount) {
readers.add(launch {
locker.acquireRead(key)
delay(100.milliseconds)
locker.releaseRead(key)
})
}
delay(50.milliseconds) // Let readers acquire
var writerExecuted = false
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
delay(50.milliseconds)
assertFalse(writerExecuted, "Writer should wait for all readers")
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should execute after all readers done")
}
@Test
fun testWriterThenMultipleReaders() = runTest {
val key = "testKey"
locker.lockWrite(key)
val readerFlags = mutableListOf<Boolean>()
val readers = List(5) {
launch {
locker.acquireRead(key)
readerFlags.add(true)
locker.releaseRead(key)
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
delay(200.milliseconds)
assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
val readPermitsBeforeLock = locker.readSemaphore().freePermits
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } }
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(key)
readers.joinAll()
locker.unlockWrite(it)
assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
@Test
fun testCascadingLocksWithDifferentKeys() = runTest {
val executed = mutableMapOf<String, Boolean>()
launch {
locker.lockWrite("key1")
executed["write-key1-start"] = true
delay(100.milliseconds)
locker.unlockWrite("key1")
executed["write-key1-end"] = true
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
delay(50.milliseconds)
launch {
locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
delay(200.milliseconds)
assertTrue(executed["write-key1-start"] == true)
assertTrue(executed["read-key2"] == true)
assertTrue(executed["write-key1-end"] == true)
}
@Test
fun testReleaseWithoutAcquireReturnsFalse() = runTest {
assertFalse(locker.releaseRead(), "Release without acquire should return false")
assertFalse(locker.releaseRead("key1"), "Release without acquire should return false")
}
@Test
fun testUnlockWithoutLockReturnsFalse() = runTest {
assertFalse(locker.unlockWrite(), "Unlock without lock should return false")
assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false")
}
@Test
fun testProperReleaseReturnsTrue() = runTest {
locker.acquireRead()
assertTrue(locker.releaseRead(), "Release after acquire should return true")
locker.acquireRead("key1")
assertTrue(locker.releaseRead("key1"), "Release after acquire should return true")
}
@Test
fun testProperUnlockReturnsTrue() = runTest {
locker.lockWrite()
assertTrue(locker.unlockWrite(), "Unlock after lock should return true")
locker.lockWrite("key1")
assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true")
}
// ==================== Stress Tests ====================
@Test
fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) {
val operations = 100
val keys = listOf("key1", "key2", "key3", "key4", "key5")
val jobs = mutableListOf<Job>()
repeat(operations) { i ->
val key = keys[i % keys.size]
when (i % 4) {
0 -> jobs.add(launch {
locker.acquireRead(key)
delay(10.milliseconds)
locker.releaseRead(key)
})
1 -> jobs.add(launch {
locker.lockWrite(key)
delay(10.milliseconds)
locker.unlockWrite(key)
})
2 -> jobs.add(launch {
locker.acquireRead()
delay(10.milliseconds)
locker.releaseRead()
})
3 -> jobs.add(launch {
locker.lockWrite()
delay(10.milliseconds)
locker.unlockWrite()
})
}
}
jobs.joinAll()
// If we reach here without deadlock or exceptions, test passes
}
@Test
fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) {
val key = "testKey"
var writerExecuted = false
// Start continuous readers
val readers = List(10) {
launch {
repeat(5) {
locker.acquireRead(key)
delay(50.milliseconds)
locker.releaseRead(key)
delay(10.milliseconds)
}
}
}
delay(100.milliseconds)
// Try to acquire write lock
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should eventually execute")
}
}

View File

@@ -26,7 +26,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
@@ -44,7 +44,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
@@ -60,7 +60,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
@@ -76,7 +76,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }

View File

@@ -1,9 +1,15 @@
interface InjectedExecOps {
@Inject //@javax.inject.Inject
ExecOperations getExecOps()
}
private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
def injected = project.objects.newInstance(InjectedExecOps)
injected.execOps.exec {
commandLine 'chmod', "+x", './changelog_parser.sh'
}
exec {
injected.execOps.exec {
standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md'
}

View File

@@ -8,8 +8,8 @@ android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
# https://github.com/google/ksp/issues/2491
ksp.useKSP2=false
## https://github.com/google/ksp/issues/2491
#ksp.useKSP2=false
# JS NPM
@@ -18,5 +18,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.26.5
android_code_version=304
version=0.28.1
android_code_version=310

View File

@@ -1,38 +1,40 @@
[versions]
kt = "2.2.20"
kt-serialization = "1.9.0"
kt = "2.3.0"
kt-serialization = "1.10.0"
kt-coroutines = "1.10.2"
kotlinx-browser = "0.5.0"
kslog = "1.5.1"
kslog = "1.5.2"
jb-compose = "1.8.2"
jb-exposed = "0.61.0"
jb-dokka = "2.0.0"
jb-compose = "1.10.0"
jb-compose-material3 = "1.10.0-alpha05"
jb-compose-icons = "1.7.8"
jb-exposed = "1.0.0"
jb-dokka = "2.1.0"
# 3.50.3.0 contains bug https://github.com/InsanusMokrassar/MicroUtils/actions/runs/18138301958/job/51629588088
# 3.51.0.0 contains bug, checking with ./gradlew :micro_utils.repos.exposed:jvmTest
sqlite = "3.50.1.0"
korlibs = "5.4.0"
uuid = "0.8.4"
ktor = "3.3.0"
ktor = "3.4.0"
gh-release = "2.5.2"
koin = "4.1.1"
okio = "3.16.0"
okio = "3.16.4"
ksp = "2.2.20-2.0.3"
ksp = "2.3.4"
kotlin-poet = "2.2.0"
versions = "0.52.0"
nmcp = "1.1.0"
versions = "0.53.0"
nmcp = "1.2.1"
android-gradle = "8.10.+"
android-gradle = "8.12.+"
dexcount = "4.0.0"
android-coreKtx = "1.17.0"
@@ -89,7 +91,8 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" }
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose-material3" }
jb-compose-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "jb-compose-icons" }
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }

View File

@@ -1,6 +1,9 @@
kotlin {
androidTarget {
publishAllLibraryVariants()
publishLibraryVariants(
"release",
"debug",
)
compilations.all {
kotlinOptions {
jvmTarget = "17"

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -27,6 +27,7 @@ import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_ksp.generator.safeClassName
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin
@@ -240,9 +241,14 @@ class Processor(
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = safeClassName { it.type }
val targetType = runCatching {
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
type.parameterizedBy(
*withNoSuchElementWorkaround(emptyArray()) {
it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type
}
)
}.getOrElse { e ->
when (e) {
is IllegalArgumentException if (e.message ?.contains("no type argument") == true) -> return@getOrElse type
is KSTypeNotPresentException -> e.ksType.toClassName()
}
if (e is KSTypesNotPresentException) {
@@ -251,14 +257,32 @@ class Processor(
throw e
}
}.copy(
nullable = it.nullable
nullable = withNoSuchElementWorkaround(true) { it.nullable }
)
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
addCodeForType(
targetType,
it.name,
withNoSuchElementWorkaround(true) {
it.nullable
},
withNoSuchElementWorkaround(true) {
it.generateSingle
},
withNoSuchElementWorkaround(true) {
it.generateFactory
}
)
}
ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach {
val targetType = TypeVariableName("T", Any::class)
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
addCodeForType(
targetType = targetType,
name = it.name,
nullable = withNoSuchElementWorkaround(true) { it.nullable },
generateSingle = withNoSuchElementWorkaround(true) { it.generateSingle },
generateFactory = withNoSuchElementWorkaround(true) { it.generateFactory }
)
}
}.build().let {
File(

View File

@@ -10,6 +10,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded
@@ -25,7 +26,11 @@ class Processor(
) {
val rootAnnotation = ksClassDeclaration.getAnnotationsByType(ClassCastsIncluded::class).first()
val (includeRegex: Regex?, excludeRegex: Regex?) = rootAnnotation.let {
it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex)
withNoSuchElementWorkaround("") {
it.typesRegex
}.takeIf { it.isNotEmpty() } ?.let(::Regex) to withNoSuchElementWorkaround("") {
it.excludeRegex
}.takeIf { it.isNotEmpty() } ?.let(::Regex)
}
val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
@@ -49,7 +54,9 @@ class Processor(
when {
potentialSubtype === ksClassDeclaration -> {}
potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class) -> return@forEach
potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel(rootAnnotation.levelsToInclude.takeIf { it >= 0 }) -> return@forEach
potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel(
withNoSuchElementWorkaround(-1) { rootAnnotation.levelsToInclude }.takeIf { it >= 0 }
) -> return@forEach
excludeRegex ?.matches(simpleName) == true -> return@forEach
includeRegex ?.matches(simpleName) == false -> {}
else -> classesSubtypes.getOrPut(ksClassDeclaration) { mutableSetOf() }.add(potentialSubtype)
@@ -96,7 +103,9 @@ class Processor(
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix
val prefix = withNoSuchElementWorkaround("") {
it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix
}
it.writeFile(prefix = prefix, suffix = "ClassCasts") {
FileSpec.builder(
it.packageName.asString(),

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_ksp.generator
inline fun <T> withNoSuchElementWorkaround(
default: T,
block: () -> T
): T = runCatching(block).getOrElse {
if (it is NoSuchElementException) {
default
} else {
throw it
}
}

View File

@@ -12,6 +12,7 @@ import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.buildSubFileName
import dev.inmo.micro_ksp.generator.companion
import dev.inmo.micro_ksp.generator.findSubClasses
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
@@ -113,7 +114,7 @@ class Processor(
val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(),
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false
).distinct()
val subClassesNames = subClasses.filter {
it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0
@@ -164,7 +165,15 @@ class Processor(
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedWorkaroundAnnotation) ?.prefix ?.takeIf {
val prefix = runCatching {
(it.getGenerateSealedWorkaroundAnnotation) ?.prefix
}.getOrElse {
if (it is NoSuchElementException) {
""
} else {
throw it
}
} ?.takeIf {
it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
@@ -184,7 +193,9 @@ class Processor(
}
}
(resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf {
val prefix = withNoSuchElementWorkaround("") {
(it.getGenerateSealedTypesWorkaroundAnnotation)?.prefix
} ?.takeIf {
it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") {

View File

@@ -16,6 +16,7 @@ import com.squareup.kotlinpoet.ksp.toTypeName
import dev.inmo.micro_ksp.generator.convertToClassName
import dev.inmo.micro_ksp.generator.convertToClassNames
import dev.inmo.micro_ksp.generator.findSubClasses
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.variations.GenerateVariations
import dev.inmo.micro_utils.ksp.variations.GenerationVariant
@@ -218,7 +219,9 @@ class Processor(
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach {
val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf {
val prefix = withNoSuchElementWorkaround("") {
(it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix
} ?.takeIf {
it.isNotEmpty()
} ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "GeneratedVariation") {

View File

@@ -23,7 +23,9 @@ dependencies {
implementation libs.ktor.client.java
}
mainClassName="MainKt"
application {
mainClass = "MainKt"
}
java {
sourceCompatibility = JavaVersion.VERSION_17

View File

@@ -14,7 +14,7 @@ kotlin {
}
jvmMain {
dependencies {
api libs.jb.exposed
api libs.jb.exposed.jdbc
}
}
}

View File

@@ -1,6 +1,8 @@
package dev.inmo.micro_utils.pagination
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.v1.core.Expression
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.jdbc.Query
fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) =
limit(with.size)

View File

@@ -2,8 +2,8 @@ package dev.inmo.micro_utils.repos.exposed
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
abstract class AbstractExposedReadCRUDRepo<ObjectType, IdType>(
tableName: String

View File

@@ -4,9 +4,12 @@ import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.WriteCRUDRepo
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.statements.InsertStatement
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update
import java.util.Objects
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
@@ -93,7 +96,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
return transaction(db = database) {
update(
{
selectById(this, id)
selectById( id)
}
) {
update(id, value, it as UpdateBuilder<Int>)
@@ -102,7 +105,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
if (it > 0) {
transaction(db = database) {
selectAll().where {
selectById(this, id)
selectById(id)
}.limit(1).firstOrNull() ?.asObject
}
} else {
@@ -137,7 +140,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
override suspend fun deleteById(ids: List<IdType>) {
onBeforeDelete(ids)
transaction(db = database) {
val deleted = deleteWhere { selectByIds(it, ids) }
val deleted = deleteWhere { selectByIds(ids) }
if (deleted == ids.size) {
ids
} else {

View File

@@ -1,6 +1,6 @@
package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Table
typealias ColumnAllocator<T> = Table.() -> Column<T>

View File

@@ -1,10 +1,10 @@
package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNotNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.isNotNull
import org.jetbrains.exposed.v1.core.isNull
import org.jetbrains.exposed.v1.core.neq
fun <T> Column<T?>.eqOrIsNull(
value: T?

View File

@@ -1,12 +1,14 @@
package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.or
interface CommonExposedRepo<IdType, ObjectType> : ExposedRepo {
val ResultRow.asObject: ObjectType
val ResultRow.asId: IdType
val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean>
val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
val selectById: (IdType) -> Op<Boolean>
val selectByIds: (List<IdType>) -> Op<Boolean>
get() = {
if (it.isEmpty()) {
Op.FALSE

View File

@@ -1,5 +1,3 @@
package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.*
interface ExposedCRUDRepo<ObjectType, IdType> : CommonExposedRepo<IdType, ObjectType>

View File

@@ -1,7 +1,11 @@
package dev.inmo.micro_utils.repos.exposed
import dev.inmo.micro_utils.repos.Repo
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.v1.core.FieldSet
import org.jetbrains.exposed.v1.core.Transaction
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.Query
import org.jetbrains.exposed.v1.jdbc.selectAll
interface ExposedRepo : Repo, FieldSet {
val database: Database

View File

@@ -1,11 +1,15 @@
package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SchemaUtils.addMissingColumnsStatements
import org.jetbrains.exposed.sql.SchemaUtils.checkMappingConsistence
import org.jetbrains.exposed.sql.SchemaUtils.createStatements
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.Transaction
import org.jetbrains.exposed.v1.core.exposedLogger
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.SchemaUtils.addMissingColumnsStatements
import org.jetbrains.exposed.v1.jdbc.SchemaUtils.checkMappingConsistence
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
/**
* Code in this function mostly duplicates Exposed [SchemaUtils.createMissingTablesAndColumns]. It made due to deprecation
@@ -41,9 +45,9 @@ fun initTablesInTransaction(vararg tables: Table, database: Database, inBatch: B
}
}
with(TransactionManager.current()) {
db.dialect.resetCaches()
db.dialectMetadata.resetCaches()
val createStatements = logTimeSpent("Preparing create tables statements", withLogs) {
createStatements(*tables)
SchemaUtils.createStatements(*tables)
}
logTimeSpent("Executing create tables statements", withLogs) {
execStatements(inBatch, createStatements)
@@ -66,7 +70,7 @@ fun initTablesInTransaction(vararg tables: Table, database: Database, inBatch: B
execStatements(inBatch, modifyTablesStatements)
commit()
}
db.dialect.resetCaches()
db.dialectMetadata.resetCaches()
}
}
}

View File

@@ -3,9 +3,14 @@ package dev.inmo.micro_utils.repos.exposed.keyvalue
import dev.inmo.micro_utils.repos.KeyValueRepo
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction
import kotlinx.coroutines.flow.update
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.deleteAll
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.update
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
abstract class AbstractExposedKeyValueRepo<Key, Value>(
override val database: Database,
@@ -54,7 +59,7 @@ abstract class AbstractExposedKeyValueRepo<Key, Value>(
override suspend fun unset(toUnset: List<Key>) {
transaction(database) {
toUnset.mapNotNull { item ->
if (deleteWhere { selectById(it, item) } > 0) {
if (deleteWhere { selectById(item) } > 0) {
item
} else {
null
@@ -69,7 +74,7 @@ abstract class AbstractExposedKeyValueRepo<Key, Value>(
transaction(database) {
toUnset.flatMap {
val keys = selectAll().where { selectByValue(it) }.mapNotNull { it.asKey }
deleteWhere { selectByIds(it, keys) }
deleteWhere { selectByIds(keys) }
keys
}
}.distinct().forEach {

View File

@@ -4,8 +4,12 @@ import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.*
import dev.inmo.micro_utils.repos.exposed.utils.selectPaginated
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
abstract class AbstractExposedReadKeyValueRepo<Key, Value>(
override val database: Database,
@@ -21,7 +25,7 @@ abstract class AbstractExposedReadKeyValueRepo<Key, Value>(
abstract val ResultRow.asKey: Key
override val ResultRow.asId: Key
get() = asKey
abstract val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean>
abstract val selectByValue: (Value) -> Op<Boolean>
override suspend fun get(k: Key): Value? = transaction(database) {
selectAll().where { selectById(k) }.limit(1).firstOrNull() ?.asObject

View File

@@ -4,11 +4,14 @@ import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.deleteAll
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update
open class ExposedKeyValueRepo<Key, Value>(
database: Database,

View File

@@ -1,13 +1,13 @@
package dev.inmo.micro_utils.repos.exposed.keyvalue
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.*
import dev.inmo.micro_utils.repos.exposed.utils.selectPaginated
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.Database
open class ExposedReadKeyValueRepo<Key, Value>(
database: Database,
@@ -20,10 +20,10 @@ open class ExposedReadKeyValueRepo<Key, Value>(
val valueColumn: Column<Value> = valueColumnAllocator()
override val ResultRow.asKey: Key
get() = get(keyColumn)
override val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean> = { valueColumn.eq(it) }
override val selectByValue: (Value) -> Op<Boolean> = { valueColumn.eq(it) }
override val ResultRow.asObject: Value
get() = get(valueColumn)
override val selectById: ISqlExpressionBuilder.(Key) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectById: (Key) -> Op<Boolean> = { keyColumn.eq(it) }
override val primaryKey: Table.PrimaryKey
get() = PrimaryKey(keyColumn, valueColumn)

View File

@@ -3,9 +3,13 @@ package dev.inmo.micro_utils.repos.exposed.onetomany
import dev.inmo.micro_utils.repos.KeyValuesRepo
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.batchInsert
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
abstract class AbstractExposedKeyValuesRepo<Key, Value>(
override val database: Database,
@@ -60,7 +64,7 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>(
val oldObjects = selectAll().where { selectByIds(toSet.keys.toList()) }.map { it.asKey to it.asObject }
deleteWhere {
selectByIds(it, toSet.keys.toList())
selectByIds(toSet.keys.toList())
}
val inserted = batchInsert(
prepreparedData,
@@ -104,7 +108,7 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>(
transaction(database) {
toRemove.keys.flatMap { k ->
toRemove[k] ?.mapNotNull { v ->
if (deleteWhere { selectById(it, k).and(SqlExpressionBuilder.selectByValue(v)) } > 0 ) {
if (deleteWhere { selectById(k).and(selectByValue(v)) } > 0 ) {
k to v
} else {
null
@@ -118,7 +122,7 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>(
override suspend fun clear(k: Key) {
transaction(database) {
deleteWhere { selectById(it, k) }
deleteWhere { selectById(k) }
}.also { _onDataCleared.emit(k) }
}
}

View File

@@ -4,8 +4,14 @@ import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.exposed.*
import dev.inmo.micro_utils.repos.exposed.utils.selectPaginated
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
abstract class AbstractExposedReadKeyValuesRepo<Key, Value>(
override val database: Database,
@@ -17,7 +23,7 @@ abstract class AbstractExposedReadKeyValuesRepo<Key, Value>(
abstract val ResultRow.asKey: Key
override val ResultRow.asId: Key
get() = asKey
abstract val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean>
abstract val selectByValue: (Value) -> Op<Boolean>
override suspend fun count(k: Key): Long = transaction(database) { selectAll().where { selectById(k) }.count() }

View File

@@ -4,10 +4,13 @@ import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
typealias ExposedOneToManyKeyValueRepo1<Key, Value> = ExposedKeyValuesRepo<Key, Value>
open class ExposedKeyValuesRepo<Key, Value>(
@@ -34,7 +37,7 @@ open class ExposedKeyValuesRepo<Key, Value>(
get() = _onDataCleared
override suspend fun add(toAdd: Map<Key, List<Value>>) {
transaction(database) {
transaction (database) {
toAdd.keys.flatMap { k ->
toAdd[k] ?.mapNotNull { v ->
if (selectAll().where { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
@@ -73,7 +76,7 @@ open class ExposedKeyValuesRepo<Key, Value>(
override suspend fun removeWithValue(v: Value) {
transaction(database) {
val keys = selectAll().where { selectByValue(v) }.map { it.asKey }
deleteWhere { SqlExpressionBuilder.selectByValue(v) }
deleteWhere { selectByValue(v) }
keys
}.forEach {
_onValueRemoved.emit(it to v)

View File

@@ -2,7 +2,11 @@ package dev.inmo.micro_utils.repos.exposed.onetomany
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.exposed.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.Database
typealias ExposedReadOneToManyKeyValueRepo<Key, Value> = ExposedReadKeyValuesRepo<Key, Value>
@@ -15,10 +19,10 @@ open class ExposedReadKeyValuesRepo<Key, Value>(
override val keyColumn: Column<Key> = keyColumnAllocator()
override val ResultRow.asKey: Key
get() = get(keyColumn)
override val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean> = { valueColumn.eq(it) }
override val selectByValue: (Value) -> Op<Boolean> = { valueColumn.eq(it) }
override val ResultRow.asObject: Value
get() = get(valueColumn)
override val selectById: ISqlExpressionBuilder.(Key) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectById: (Key) -> Op<Boolean> = { keyColumn.eq(it) }
val valueColumn: Column<Value> = valueColumnAllocator()
init { initTable() }

View File

@@ -1,7 +1,10 @@
package dev.inmo.micro_utils.repos.exposed.utils
import dev.inmo.micro_utils.pagination.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.v1.core.Expression
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.jdbc.Query
fun <T> Query.selectPaginated(
pagination: Pagination,

View File

@@ -3,8 +3,12 @@ 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
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update
/**
* Use this method to create [StandardVersionsRepo] based on [Database] with [ExposedStandardVersionsRepoProxy] as

View File

@@ -1,8 +1,8 @@
package full
import com.benasher44.uuid.uuid4
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.transactions.transactionManager
import org.sqlite.JDBC
import java.io.File
import java.sql.Connection

View File

@@ -5,12 +5,12 @@ import dev.inmo.micro_utils.repos.CRUDRepo
import dev.inmo.micro_utils.repos.common.tests.CommonCRUDRepoTests
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.statements.InsertStatement
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.Database
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -29,7 +29,7 @@ class ExposedCRUDRepoTests : CommonCRUDRepoTests() {
asId,
get(dataColumn)
)
override val selectById: ISqlExpressionBuilder.(String) -> Op<Boolean> = { idColumn.eq(it) }
override val selectById: (String) -> Op<Boolean> = { idColumn.eq(it) }
init {
initTable()

View File

@@ -4,11 +4,11 @@ import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValueRepoTests
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.Database
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -22,10 +22,10 @@ class ExposedKeyValueRepoTests : CommonKeyValueRepoTests() {
override val ResultRow.asKey: String
get() = get(keyColumn)
override val selectByValue: ISqlExpressionBuilder.(String) -> Op<Boolean> = { dataColumn.eq(it) }
override val selectByValue: (String) -> Op<Boolean> = { dataColumn.eq(it) }
override val ResultRow.asObject: String
get() = get(dataColumn)
override val selectById: ISqlExpressionBuilder.(String) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectById: (String) -> Op<Boolean> = { keyColumn.eq(it) }
init {
initTable()

View File

@@ -4,11 +4,11 @@ import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValuesRepoTests
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.onetomany.AbstractExposedKeyValuesRepo
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
import org.jetbrains.exposed.v1.jdbc.Database
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -22,10 +22,10 @@ class ExposedKeyValuesRepoTests : CommonKeyValuesRepoTests() {
override val ResultRow.asKey: String
get() = get(keyColumn)
override val selectByValue: ISqlExpressionBuilder.(String) -> Op<Boolean> = { dataColumn.eq(it) }
override val selectByValue: (String) -> Op<Boolean> = { dataColumn.eq(it) }
override val ResultRow.asObject: String
get() = get(dataColumn)
override val selectById: ISqlExpressionBuilder.(String) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectById: (String) -> Op<Boolean> = { keyColumn.eq(it) }
init {
initTable()