Krush
Krush is "a Kotlin-first type-safe ORM" developed as a database access library that maximizes the utilization of Kotlin language characteristics. Through annotation-based entity definitions and code generation, it ensures compile-time type safety and achieves perfect integration with modern Kotlin language features such as data classes, null safety, and type inference. While providing simple and intuitive APIs, it supports complex query construction, relationship mapping, and batch processing, offering a clean and readable database access layer that feels natural to Kotlin developers.
GitHub Overview
Topics
Star History
Library
Krush
Overview
Krush is "a Kotlin-first type-safe ORM" developed as a database access library that maximizes the utilization of Kotlin language characteristics. Through annotation-based entity definitions and code generation, it ensures compile-time type safety and achieves perfect integration with modern Kotlin language features such as data classes, null safety, and type inference. While providing simple and intuitive APIs, it supports complex query construction, relationship mapping, and batch processing, offering a clean and readable database access layer that feels natural to Kotlin developers.
Details
Krush 2025 edition meets the needs of modern Kotlin application development with Kotlin Multiplatform support and complete Coroutines integration. The data class-centric design philosophy makes entity definitions extremely simple and maintainable, providing a natural development experience for Kotlin developers. It supports major databases including PostgreSQL, MySQL, SQLite, and H2, operating as a lightweight abstraction layer over JDBC. Supporting both Active Record and Repository patterns, it enables flexible implementation according to project requirements.
Key Features
- Kotlin-First: Full utilization of data classes and Kotlin language features
- Type Safety: Compile-time type checking and null safety
- Simple API: Intuitive interface with low learning curve
- Lightweight Design: Minimal runtime overhead
- Relationship Mapping: Support for one-to-one, one-to-many, many-to-many relationships
- Multiplatform: Operation on JVM, Android, and Kotlin/Native
Pros and Cons
Pros
- Natural and concise code writing leveraging Kotlin language characteristics
- High maintainability and readability with data class-centric design
- Early bug detection through compile-time type safety
- Lightweight library with minimal performance overhead
- Excellent integration with Coroutines for smooth asynchronous processing
- Low learning curve, manageable with existing Kotlin knowledge
Cons
- Relatively new library with developing ecosystem
- Limitations on complex queries and advanced SQL features
- Limited documentation and community resources
- Lack of large-scale enterprise features
- Learning cost when migrating from other languages
- Limited database-specific optimization features
Reference Pages
Code Examples
Setup
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
kotlin("kapt") version "1.9.22"
}
dependencies {
implementation("pl.touk.krush:krush-annotation-processor:0.8.2")
implementation("pl.touk.krush:krush-runtime:0.8.2")
kapt("pl.touk.krush:krush-annotation-processor:0.8.2")
// Database drivers
implementation("org.postgresql:postgresql:42.7.1")
implementation("mysql:mysql-connector-java:8.0.33")
implementation("com.h2database:h2:2.2.224")
// Connection pool
implementation("com.zaxxer:HikariCP:5.1.0")
// Coroutines support
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
// Annotation processing configuration
kapt {
arguments {
arg("krush.generated.package", "com.example.generated")
}
}
-- Database schema example
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
color VARCHAR(7) DEFAULT '#000000'
);
CREATE TABLE post_tags (
post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE,
tag_id BIGINT REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
Basic Usage
import pl.touk.krush.*
import java.time.LocalDateTime
// Entity definitions with Krush annotations
@Entity
@Table("users")
data class User(
@Id
@GeneratedValue
val id: Long? = null,
@Column("name")
val name: String,
@Column("email")
val email: String,
@Column("age")
val age: Int,
@Column("created_at")
val createdAt: LocalDateTime = LocalDateTime.now(),
@Column("updated_at")
val updatedAt: LocalDateTime = LocalDateTime.now()
)
@Entity
@Table("posts")
data class Post(
@Id
@GeneratedValue
val id: Long? = null,
@Column("title")
val title: String,
@Column("content")
val content: String?,
@Column("user_id")
val userId: Long,
@Column("published")
val published: Boolean = false,
@Column("created_at")
val createdAt: LocalDateTime = LocalDateTime.now()
) {
// Relationships
@ManyToOne
@JoinColumn("user_id")
lateinit var user: User
@ManyToMany
@JoinTable("post_tags", joinColumns = [JoinColumn("post_id")],
inverseJoinColumns = [JoinColumn("tag_id")])
var tags: List<Tag> = emptyList()
}
@Entity
@Table("tags")
data class Tag(
@Id
@GeneratedValue
val id: Long? = null,
@Column("name")
val name: String,
@Column("color")
val color: String = "#000000"
)
// Database configuration
class DatabaseConfig {
companion object {
fun createDataSource(): DataSource {
return HikariDataSource().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/krush_demo"
username = "postgres"
password = "password"
maximumPoolSize = 10
minimumIdle = 5
}
}
}
}
// Repository implementation
class UserRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Create user
suspend fun createUser(name: String, email: String, age: Int): User? = withContext(Dispatchers.IO) {
val user = User(name = name, email = email, age = age)
krush.save(user)
}
// Find user by ID
suspend fun findUserById(id: Long): User? = withContext(Dispatchers.IO) {
krush.findById<User>(id)
}
// Find user by email
suspend fun findUserByEmail(email: String): User? = withContext(Dispatchers.IO) {
krush.findBy<User> { User::email eq email }
}
// Find all users
suspend fun findAllUsers(): List<User> = withContext(Dispatchers.IO) {
krush.findAll<User>()
}
// Search users by name pattern
suspend fun searchUsersByName(namePattern: String): List<User> = withContext(Dispatchers.IO) {
krush.findBy<User> { User::name like "%$namePattern%" }
}
// Find users by age range
suspend fun findUsersByAgeRange(minAge: Int, maxAge: Int): List<User> = withContext(Dispatchers.IO) {
krush.findBy<User> {
(User::age gte minAge) and (User::age lte maxAge)
}
}
// Update user
suspend fun updateUser(user: User): User? = withContext(Dispatchers.IO) {
val updatedUser = user.copy(updatedAt = LocalDateTime.now())
krush.update(updatedUser)
}
// Delete user
suspend fun deleteUser(id: Long): Boolean = withContext(Dispatchers.IO) {
krush.deleteById<User>(id) > 0
}
// Count users
suspend fun countUsers(): Long = withContext(Dispatchers.IO) {
krush.count<User>()
}
// Pagination
suspend fun findUsersWithPagination(page: Int, size: Int): List<User> = withContext(Dispatchers.IO) {
krush.findBy<User>(
limit = size,
offset = page * size
) { User::createdAt.desc() }
}
}
// Usage example
suspend fun demonstrateBasicOperations() {
val dataSource = DatabaseConfig.createDataSource()
val userRepository = UserRepository(dataSource)
// Create users
val user1 = userRepository.createUser("Alice Johnson", "[email protected]", 28)
val user2 = userRepository.createUser("Bob Smith", "[email protected]", 32)
val user3 = userRepository.createUser("Charlie Brown", "[email protected]", 25)
println("Created users: ${listOfNotNull(user1, user2, user3)}")
// Find user by email
val foundUser = userRepository.findUserByEmail("[email protected]")
println("Found user by email: $foundUser")
// Search users by name
val searchResults = userRepository.searchUsersByName("B")
println("Users with 'B' in name: $searchResults")
// Find users by age range
val youngUsers = userRepository.findUsersByAgeRange(25, 30)
println("Users aged 25-30: $youngUsers")
// Update user
user1?.let { user ->
val updatedUser = userRepository.updateUser(
user.copy(name = "Alice Johnson Smith", age = 29)
)
println("Updated user: $updatedUser")
}
// Count and pagination
val totalUsers = userRepository.countUsers()
println("Total users: $totalUsers")
val firstPage = userRepository.findUsersWithPagination(page = 0, size = 2)
println("First page users: $firstPage")
}
Relationships and Queries
class PostRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Create post with user
suspend fun createPost(title: String, content: String?, userId: Long, published: Boolean = false): Post? =
withContext(Dispatchers.IO) {
val post = Post(
title = title,
content = content,
userId = userId,
published = published
)
krush.save(post)
}
// Find posts by user
suspend fun findPostsByUser(userId: Long): List<Post> = withContext(Dispatchers.IO) {
krush.findBy<Post> { Post::userId eq userId }
}
// Find posts with user (JOIN)
suspend fun findPostsWithUser(): List<Pair<Post, User>> = withContext(Dispatchers.IO) {
krush.query<Post> {
select(Post::class, User::class)
from(Post::class)
join(User::class).on { Post::userId eq User::id }
orderBy(Post::createdAt.desc())
}.map { row ->
Pair(row.get(Post::class), row.get(User::class))
}
}
// Find published posts only
suspend fun findPublishedPosts(): List<Post> = withContext(Dispatchers.IO) {
krush.findBy<Post> { Post::published eq true }
}
// Search posts by title
suspend fun searchPostsByTitle(titlePattern: String): List<Post> = withContext(Dispatchers.IO) {
krush.findBy<Post> { Post::title like "%$titlePattern%" }
}
// Complex query: Find users with most posts
suspend fun findUsersWithMostPosts(limit: Int = 5): List<Pair<User, Long>> = withContext(Dispatchers.IO) {
krush.query<User> {
select(User::class, count(Post::id))
from(User::class)
leftJoin(Post::class).on { User::id eq Post::userId }
groupBy(User::id)
orderBy(count(Post::id).desc())
limit(limit)
}.map { row ->
Pair(row.get(User::class), row.get(1, Long::class.java))
}
}
// Update post status
suspend fun publishPost(postId: Long): Boolean = withContext(Dispatchers.IO) {
krush.updateBy<Post>({ Post::id eq postId }) {
set(Post::published, true)
} > 0
}
// Delete posts by user
suspend fun deletePostsByUser(userId: Long): Int = withContext(Dispatchers.IO) {
krush.deleteBy<Post> { Post::userId eq userId }
}
// Bulk operations
suspend fun bulkUpdatePostPublishStatus(postIds: List<Long>, published: Boolean): Int =
withContext(Dispatchers.IO) {
krush.updateBy<Post>({ Post::id `in` postIds }) {
set(Post::published, published)
}
}
}
class TagRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Create tag
suspend fun createTag(name: String, color: String = "#000000"): Tag? = withContext(Dispatchers.IO) {
val tag = Tag(name = name, color = color)
krush.save(tag)
}
// Find tag by name
suspend fun findTagByName(name: String): Tag? = withContext(Dispatchers.IO) {
krush.findBy<Tag> { Tag::name eq name }
}
// Find all tags
suspend fun findAllTags(): List<Tag> = withContext(Dispatchers.IO) {
krush.findAll<Tag>()
}
// Find or create tag
suspend fun findOrCreateTag(name: String, color: String = "#000000"): Tag = withContext(Dispatchers.IO) {
findTagByName(name) ?: createTag(name, color)!!
}
// Find popular tags (by post count)
suspend fun findPopularTags(limit: Int = 10): List<Pair<Tag, Long>> = withContext(Dispatchers.IO) {
krush.query<Tag> {
select(Tag::class, count())
from(Tag::class)
join("post_tags").on { Tag::id eq column("tag_id") }
groupBy(Tag::id)
orderBy(count().desc())
limit(limit)
}.map { row ->
Pair(row.get(Tag::class), row.get(1, Long::class.java))
}
}
}
// Usage example for relationships
suspend fun demonstrateRelationshipsAndQueries() {
val dataSource = DatabaseConfig.createDataSource()
val userRepository = UserRepository(dataSource)
val postRepository = PostRepository(dataSource)
val tagRepository = TagRepository(dataSource)
// Create user and posts
val user = userRepository.createUser("Content Creator", "[email protected]", 30)
user?.let { u ->
postRepository.createPost("First Post", "This is my first post!", u.id!!, true)
postRepository.createPost("Draft Post", "This is a draft", u.id!!, false)
postRepository.createPost("Another Post", "More content here", u.id!!, true)
}
// Find posts with users
val postsWithUsers = postRepository.findPostsWithUser()
println("Posts with users: $postsWithUsers")
// Find published posts
val publishedPosts = postRepository.findPublishedPosts()
println("Published posts: $publishedPosts")
// Search posts
val searchResults = postRepository.searchPostsByTitle("Post")
println("Posts containing 'Post': $searchResults")
// Users with most posts
val topUsers = postRepository.findUsersWithMostPosts(3)
println("Top users by post count: $topUsers")
// Create tags
val tags = listOf(
tagRepository.findOrCreateTag("Technology", "#3498db"),
tagRepository.findOrCreateTag("Programming", "#e74c3c"),
tagRepository.findOrCreateTag("Kotlin", "#f39c12")
)
println("Created/found tags: $tags")
// Popular tags
val popularTags = tagRepository.findPopularTags(5)
println("Popular tags: $popularTags")
}
Transactions and Batch Operations
class UserService(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Transaction example
suspend fun transferPostsBetweenUsers(fromUserId: Long, toUserId: Long): Boolean = withContext(Dispatchers.IO) {
try {
krush.transaction {
// Verify both users exist
val fromUser = krush.findById<User>(fromUserId)
val toUser = krush.findById<User>(toUserId)
if (fromUser == null || toUser == null) {
throw IllegalArgumentException("One or both users not found")
}
// Transfer posts
val transferCount = krush.updateBy<Post>({ Post::userId eq fromUserId }) {
set(Post::userId, toUserId)
}
println("Transferred $transferCount posts from user $fromUserId to $toUserId")
}
true
} catch (e: Exception) {
println("Transfer failed: ${e.message}")
false
}
}
// Batch user creation
suspend fun batchCreateUsers(usersData: List<Triple<String, String, Int>>): List<User> =
withContext(Dispatchers.IO) {
krush.transaction {
usersData.mapNotNull { (name, email, age) ->
krush.save(User(name = name, email = email, age = age))
}
}
}
// Complex transaction: Create user with initial posts and tags
suspend fun createUserWithContent(
userName: String,
userEmail: String,
userAge: Int,
posts: List<Pair<String, String>>,
tagNames: List<String>
): UserCreationResult = withContext(Dispatchers.IO) {
try {
krush.transaction {
// Create user
val user = krush.save(User(name = userName, email = userEmail, age = userAge))
?: throw RuntimeException("Failed to create user")
// Create or find tags
val tags = tagNames.map { tagName ->
krush.findBy<Tag> { Tag::name eq tagName }.firstOrNull()
?: krush.save(Tag(name = tagName))!!
}
// Create posts
val createdPosts = posts.map { (title, content) ->
krush.save(Post(
title = title,
content = content,
userId = user.id!!,
published = false
))!!
}
UserCreationResult.Success(user, createdPosts, tags)
}
} catch (e: Exception) {
UserCreationResult.Error(e.message ?: "Unknown error")
}
}
// Bulk update with conditions
suspend fun bulkUpdateUserAges(ageUpdates: Map<Long, Int>): Int = withContext(Dispatchers.IO) {
krush.transaction {
var totalUpdated = 0
ageUpdates.forEach { (userId, newAge) ->
val updated = krush.updateBy<User>({ User::id eq userId }) {
set(User::age, newAge)
set(User::updatedAt, LocalDateTime.now())
}
totalUpdated += updated
}
totalUpdated
}
}
// Conditional cleanup operation
suspend fun cleanupInactiveUsers(daysSinceLastPost: Int): CleanupResult = withContext(Dispatchers.IO) {
krush.transaction {
val cutoffDate = LocalDateTime.now().minusDays(daysSinceLastPost.toLong())
// Find inactive users
val inactiveUserIds = krush.query<User> {
select(User::id)
from(User::class)
where {
not(exists {
select(literal(1))
from(Post::class)
where { (Post::userId eq User::id) and (Post::createdAt gt cutoffDate) }
})
}
}.map { it.get(User::id) }
if (inactiveUserIds.isEmpty()) {
return@transaction CleanupResult(0, 0, emptyList())
}
// Delete posts first (foreign key constraint)
val deletedPosts = krush.deleteBy<Post> { Post::userId `in` inactiveUserIds }
// Delete users
val deletedUsers = krush.deleteBy<User> { User::id `in` inactiveUserIds }
CleanupResult(deletedUsers, deletedPosts, inactiveUserIds)
}
}
// Safe batch operation with validation
suspend fun safeBatchCreateUsers(usersData: List<Triple<String, String, Int>>): BatchCreationResult =
withContext(Dispatchers.IO) {
// Validation
val errors = mutableListOf<String>()
usersData.forEachIndexed { index, (name, email, age) ->
if (name.isBlank()) errors.add("Empty name at index $index")
if (!email.contains("@")) errors.add("Invalid email at index $index: $email")
if (age < 0 || age > 150) errors.add("Invalid age at index $index: $age")
}
if (errors.isNotEmpty()) {
return@withContext BatchCreationResult.ValidationError(errors)
}
try {
val createdUsers = krush.transaction {
// Check for duplicate emails
val emails = usersData.map { it.second }
val existingEmails = krush.findBy<User> { User::email `in` emails }
.map { it.email }
if (existingEmails.isNotEmpty()) {
throw RuntimeException("Duplicate emails found: ${existingEmails.joinToString()}")
}
// Batch creation
usersData.mapNotNull { (name, email, age) ->
krush.save(User(name = name, email = email, age = age))
}
}
BatchCreationResult.Success(createdUsers)
} catch (e: Exception) {
BatchCreationResult.Error(e.message ?: "Batch creation failed")
}
}
}
// Result classes
sealed class UserCreationResult {
data class Success(val user: User, val posts: List<Post>, val tags: List<Tag>) : UserCreationResult()
data class Error(val message: String) : UserCreationResult()
}
sealed class BatchCreationResult {
data class Success(val users: List<User>) : BatchCreationResult()
data class ValidationError(val errors: List<String>) : BatchCreationResult()
data class Error(val message: String) : BatchCreationResult()
}
data class CleanupResult(
val deletedUsers: Int,
val deletedPosts: Int,
val inactiveUserIds: List<Long>
)
// Usage example for transactions and batch operations
suspend fun demonstrateTransactionsAndBatch() {
val dataSource = DatabaseConfig.createDataSource()
val userService = UserService(dataSource)
// Batch user creation
val newUsersData = listOf(
Triple("Emma Wilson", "[email protected]", 26),
Triple("David Chen", "[email protected]", 31),
Triple("Sarah Johnson", "[email protected]", 29)
)
when (val result = userService.safeBatchCreateUsers(newUsersData)) {
is BatchCreationResult.Success -> {
println("✓ Batch created ${result.users.size} users: ${result.users}")
}
is BatchCreationResult.ValidationError -> {
println("✗ Validation errors: ${result.errors}")
}
is BatchCreationResult.Error -> {
println("✗ Batch creation failed: ${result.message}")
}
}
// Create user with content
when (val result = userService.createUserWithContent(
userName = "Tech Blogger",
userEmail = "[email protected]",
userAge = 32,
posts = listOf(
"Getting Started with Kotlin" to "Kotlin is a great language...",
"Database Access with Krush" to "Let's explore Krush ORM..."
),
tagNames = listOf("Kotlin", "Programming", "Tutorial")
)) {
is UserCreationResult.Success -> {
println("✓ Created user with content:")
println(" User: ${result.user}")
println(" Posts: ${result.posts}")
println(" Tags: ${result.tags}")
}
is UserCreationResult.Error -> {
println("✗ User creation failed: ${result.message}")
}
}
// Cleanup inactive users
val cleanupResult = userService.cleanupInactiveUsers(30)
println("Cleanup result: $cleanupResult")
}
Error Handling
// Custom exceptions
sealed class KrushException(message: String, cause: Throwable? = null) : Exception(message, cause) {
class EntityNotFoundException(entityType: String, id: Any) : KrushException("$entityType not found with id: $id")
class DuplicateKeyException(message: String) : KrushException("Duplicate key violation: $message")
class ValidationException(message: String) : KrushException("Validation failed: $message")
class DatabaseException(message: String, cause: Throwable? = null) : KrushException("Database error: $message", cause)
class TransactionException(message: String, cause: Throwable? = null) : KrushException("Transaction failed: $message", cause)
}
// Safe result wrapper
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: KrushException) : Result<Nothing>()
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Success -> Success(transform(data))
is Error -> this
}
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
is Success -> transform(data)
is Error -> this
}
fun getOrNull(): T? = when (this) {
is Success -> data
is Error -> null
}
}
class SafeUserRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Safe user creation with validation
suspend fun createUserSafely(name: String, email: String, age: Int): Result<User> = withContext(Dispatchers.IO) {
try {
// Input validation
if (name.isBlank()) {
return@withContext Result.Error(KrushException.ValidationException("Name cannot be empty"))
}
if (!email.contains("@") || email.length < 5) {
return@withContext Result.Error(KrushException.ValidationException("Invalid email format"))
}
if (age < 0 || age > 150) {
return@withContext Result.Error(KrushException.ValidationException("Age must be between 0 and 150"))
}
// Check for duplicate email
val existingUser = krush.findBy<User> { User::email eq email }.firstOrNull()
if (existingUser != null) {
return@withContext Result.Error(KrushException.DuplicateKeyException("Email $email already exists"))
}
// Create user
val user = krush.save(User(name = name, email = email, age = age))
if (user != null) {
Result.Success(user)
} else {
Result.Error(KrushException.DatabaseException("Failed to create user"))
}
} catch (e: Exception) {
when (e) {
is KrushException -> Result.Error(e)
else -> Result.Error(KrushException.DatabaseException("Unexpected error", e))
}
}
}
// Safe user retrieval
suspend fun getUserSafely(userId: Long): Result<User> = withContext(Dispatchers.IO) {
try {
val user = krush.findById<User>(userId)
if (user != null) {
Result.Success(user)
} else {
Result.Error(KrushException.EntityNotFoundException("User", userId))
}
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Failed to retrieve user", e))
}
}
// Safe user update
suspend fun updateUserSafely(
userId: Long,
name: String,
email: String,
age: Int
): Result<User> = withContext(Dispatchers.IO) {
try {
// Validation
if (name.isBlank()) {
return@withContext Result.Error(KrushException.ValidationException("Name cannot be empty"))
}
if (!email.contains("@")) {
return@withContext Result.Error(KrushException.ValidationException("Invalid email format"))
}
if (age < 0 || age > 150) {
return@withContext Result.Error(KrushException.ValidationException("Invalid age"))
}
val result = krush.transaction {
// Check if user exists
val existingUser = krush.findById<User>(userId)
?: throw KrushException.EntityNotFoundException("User", userId)
// Check for email conflicts (excluding current user)
val emailConflict = krush.findBy<User> {
(User::email eq email) and (User::id neq userId)
}.firstOrNull()
if (emailConflict != null) {
throw KrushException.DuplicateKeyException("Email $email is already in use")
}
// Update user
val updatedUser = existingUser.copy(
name = name,
email = email,
age = age,
updatedAt = LocalDateTime.now()
)
krush.update(updatedUser) ?: throw KrushException.DatabaseException("Failed to update user")
}
Result.Success(result)
} catch (e: KrushException) {
Result.Error(e)
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Unexpected error during update", e))
}
}
// Safe deletion with cascade check
suspend fun deleteUserSafely(userId: Long): Result<Boolean> = withContext(Dispatchers.IO) {
try {
val result = krush.transaction {
// Check if user exists
val user = krush.findById<User>(userId)
?: throw KrushException.EntityNotFoundException("User", userId)
// Check for related posts
val postCount = krush.count<Post> { Post::userId eq userId }
if (postCount > 0) {
// Delete related posts first
krush.deleteBy<Post> { Post::userId eq userId }
}
// Delete user
val deleteCount = krush.deleteById<User>(userId)
deleteCount > 0
}
Result.Success(result)
} catch (e: KrushException) {
Result.Error(e)
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Failed to delete user", e))
}
}
// Safe batch operation
suspend fun batchCreateUsersSafely(
usersData: List<Triple<String, String, Int>>
): Result<List<User>> = withContext(Dispatchers.IO) {
try {
// Validate all data first
usersData.forEachIndexed { index, (name, email, age) ->
if (name.isBlank()) {
throw KrushException.ValidationException("Name cannot be empty at index $index")
}
if (!email.contains("@")) {
throw KrushException.ValidationException("Invalid email at index $index: $email")
}
if (age < 0 || age > 150) {
throw KrushException.ValidationException("Invalid age at index $index: $age")
}
}
// Check for duplicate emails in batch
val emails = usersData.map { it.second }
if (emails.size != emails.toSet().size) {
throw KrushException.ValidationException("Duplicate emails in batch data")
}
val result = krush.transaction {
// Check for existing emails
val existingEmails = krush.findBy<User> { User::email `in` emails }
.map { it.email }
if (existingEmails.isNotEmpty()) {
throw KrushException.DuplicateKeyException("Existing emails found: ${existingEmails.joinToString()}")
}
// Batch create
usersData.mapNotNull { (name, email, age) ->
krush.save(User(name = name, email = email, age = age))
}
}
Result.Success(result)
} catch (e: KrushException) {
Result.Error(e)
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Batch creation failed", e))
}
}
// Database health check
suspend fun checkDatabaseHealth(): Result<String> = withContext(Dispatchers.IO) {
try {
val count = krush.count<User>()
Result.Success("Database connection healthy. Total users: $count")
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Database health check failed", e))
}
}
}
// Usage example for error handling
suspend fun demonstrateErrorHandling() {
val dataSource = DatabaseConfig.createDataSource()
val safeUserRepo = SafeUserRepository(dataSource)
// Test database health
when (val healthResult = safeUserRepo.checkDatabaseHealth()) {
is Result.Success -> println("✓ ${healthResult.data}")
is Result.Error -> println("✗ Health check failed: ${healthResult.exception.message}")
}
// Safe user creation
when (val result = safeUserRepo.createUserSafely("Jane Doe", "[email protected]", 28)) {
is Result.Success -> {
println("✓ Successfully created user: ${result.data}")
// Safe update
when (val updateResult = safeUserRepo.updateUserSafely(
result.data.id!!, "Jane Smith", "[email protected]", 29
)) {
is Result.Success -> println("✓ Successfully updated user: ${updateResult.data}")
is Result.Error -> println("✗ Update failed: ${updateResult.exception.message}")
}
}
is Result.Error -> println("✗ User creation failed: ${result.exception.message}")
}
// Test invalid data
when (val invalidResult = safeUserRepo.createUserSafely("", "invalid-email", -5)) {
is Result.Success -> println("This shouldn't happen")
is Result.Error -> println("✓ Correctly caught validation error: ${invalidResult.exception.message}")
}
// Test duplicate email
when (val duplicateResult = safeUserRepo.createUserSafely("Another Jane", "[email protected]", 30)) {
is Result.Success -> println("This shouldn't happen")
is Result.Error -> println("✓ Correctly caught duplicate email: ${duplicateResult.exception.message}")
}
// Test safe batch creation
val batchData = listOf(
Triple("User 1", "[email protected]", 25),
Triple("User 2", "[email protected]", 30),
Triple("", "[email protected]", 35) // Invalid name
)
when (val batchResult = safeUserRepo.batchCreateUsersSafely(batchData)) {
is Result.Success -> println("✓ Batch creation successful: ${batchResult.data}")
is Result.Error -> println("✓ Correctly caught batch validation error: ${batchResult.exception.message}")
}
// Test non-existent user retrieval
when (val result = safeUserRepo.getUserSafely(999L)) {
is Result.Success -> println("This shouldn't happen")
is Result.Error -> println("✓ Correctly caught entity not found: ${result.exception.message}")
}
}