Skip to content

Commit

Permalink
Annotations (#10)
Browse files Browse the repository at this point in the history
* Add annotations for simple table actions

* Update README.md

* Add missing type parameters after merge

* Fix README.md

* Improve update

* Add @update annotation

* Update README.md

* Add @upsert
  • Loading branch information
MineKing9534 authored Nov 6, 2024
1 parent 36e2587 commit eb5b064
Show file tree
Hide file tree
Showing 20 changed files with 396 additions and 64 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,60 @@ DataObject is an interface that your row classes can implement to simplify some
It allows calling `.insert()`, `.update()` and `.delete()` directly on the object.
It also has the methods `beforeWrite` and `afterRead` that you can override to listen to the corresponding event.

Lastly, it has a utility method that allows you to select a list of rows from another table, that reference this object with some column.
Lastly, it has a utility method that allows you to select a list of rows from another table, that reference this object with some column.

### Custom Tables
You can also create custom interfaces that allow you to predefine certain operations that can then be used later:
```kotlin
interface UserTable : Table<UserDao> {
@Select //You can use the @Select annotation to automatically generate a select statement
fun getAllUsers(): List<UserDao>

@Select //You can also select a single element. The return type of the function will determine this behavior
fun getUserByName(@KeyParameter name: String): UserDao? //All parameters with the @KeyParameter annotation will be used as a where condition when selecting (All of the parameters have to match)

@Insert //You can create insert statement functions with @Insert
fun createUser(@Parameter name: String, @Parameter age: Int): UserDao //All method parameters annotated with @Parameter will be passed to the insert. All properties that are not defined here will have the value they have after the instance was created by your instance creator

@Delete //You can create delete statement functions with @Delete
fun deleteUser(@KeyParameter id: Int): Int //As above, all parameters with @KeyParameter will be used as condition. For example a delete function without any parameters will delete all rows

//Custom function
fun getAdults() = select(where = property(UserDao::age) isGreaterThan value(18))

@Select
fun getOlderThan(@Parameter(name = "age", operation = " > ") minAge: Int): List<UserDao> //Will select all users older than minAge. You can pass a custom comparison operation as parameter to the @KeyParameter annotation. The default is " = "
@Update
fun updateName(@KeyParameter id: Int, @Parameter name: String): Int //You can combine @KeyParameter and @Parameter in @Update. As above, @KeyParameter will be used as condition while the parameters with @Parameter will update the respective columns in the rows matching the condition
}
fun main() {
//Your connection and table declaration...
//Pass the type of your interface as type parameter. KORMite will create an instance of this interface via dynamic proxy
val table = connection.getTable<_, UserTable>(name = "users", create = true) { UserDao(name = "", age = 0) }
val tom = table.createUser(name = "Tom", age = 16)
val alex = table.createUser(name = "Alex", age = 20)
assertEquals(2, table.getAllUsers().size)
assertEquals(1, table.getAdults().size)
table.deleteUser(tom.id)
assertEquals(1, table.getAllUsers().size)
assertEquals(1, table.updateName(alex.id, "Test"))
assertEquals("Test", table.getAllUsers().first().name)
}
```
Note: Using the autogenerated functions with annotations required you to keep you parameter names. In gradle you can do this with:
```gradle
kotlin {
compilerOptions {
javaParameters = true
}
}
```
Alternatively you can pass the name of the property as parameter to @Parameter (e.g. `@Parameter(name = "id")`). The same applies to @KeyParameter
9 changes: 9 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ allprojects {
}
}
}

kotlin {
jvmToolchain(21)
}

java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}

tasks.register("publishAll") {
Expand Down
59 changes: 59 additions & 0 deletions core/src/main/kotlin/de/mineking/database/Annotations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package de.mineking.database

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String = "")

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class AutoIncrement

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class AutoGenerate(val generator: String = "")

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Key

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Unique(val name: String = "")

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Reference(val table: String)

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Json(val binary: Boolean = false)



@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Parameter(val name: String = "")

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class KeyParameter(val name: String = "", val operation: String = " = ")

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Select

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Insert

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Update

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Upsert

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Delete
3 changes: 3 additions & 0 deletions core/src/main/kotlin/de/mineking/database/Nodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ fun unsafeNode(string: String, values: Map<String, Argument> = emptyMap()) = obj
override fun values(table: TableStructure<*>, column: ColumnData<*, *>?): Map<String, Argument> = values
}

@Suppress("UNCHECKED_CAST")
infix fun <T> Node<T>.to(other: Node<T>) = ((this as Any) to other) as Pair<Node<T>, Node<T>>

interface Node<T> {
companion object {
val EMPTY = unsafeNode("")
Expand Down
100 changes: 68 additions & 32 deletions core/src/main/kotlin/de/mineking/database/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,7 @@ import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import kotlin.reflect.*
import kotlin.reflect.jvm.javaField

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String = "")

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class AutoIncrement

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class AutoGenerate(val generator: String = "")

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Key

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Unique(val name: String = "")

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Reference(val table: String)

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Json(val binary: Boolean = false)
import kotlin.reflect.jvm.kotlinFunction

interface Table<T: Any> {
val structure: TableStructure<T>
Expand All @@ -52,8 +25,7 @@ interface Table<T: Any> {
fun select(where: Where = Where.EMPTY, order: Order? = null, limit: Int? = null, offset: Int? = null): QueryResult<T> = select(columns = emptyArray<KProperty<*>>(), where, order, limit, offset)

fun update(obj: T): UpdateResult<T>
fun <T> update(column: Node<T>, value: Node<T>, where: Where = Where.EMPTY): UpdateResult<Int>
fun <T> update(column: KProperty<T>, value: Node<T>, where: Where = Where.EMPTY): UpdateResult<Int> = update(property(column), value, where)
fun update(vararg columns: Pair<Node<*>, Node<*>>, where: Where = Where.EMPTY): UpdateResult<Int>

fun insert(obj: T): UpdateResult<T>
fun upsert(obj: T): UpdateResult<T>
Expand Down Expand Up @@ -82,10 +54,74 @@ abstract class TableImplementation<T: Any>(
}

override fun invoke(proxy: Any?, method: Method?, args: Array<out Any?>?): Any? {
require(method != null)

fun createCondition() = allOf(if (args == null) emptyList() else method.parameters
.mapIndexed { index, value -> index to value }
.filter { (_, it) -> it.isAnnotationPresent(KeyParameter::class.java) }
.map { (index, param) -> property<Any>(param.getAnnotation(KeyParameter::class.java)!!.name.takeIf { it.isNotBlank() } ?: param.name) + " ${ param.getAnnotation(KeyParameter::class.java)!!.operation } " + value(args[index]) }
.map { Where(it) }
)

when {
method.isAnnotationPresent(Select::class.java) -> {
val result = select(where = createCondition())

return when {
method.returnType == QueryResult::class.java -> result
method.returnType == List::class.java -> result.list()
method.kotlinFunction?.returnType?.isMarkedNullable == true -> result.findFirst()
else -> result.first()
}
}

method.isAnnotationPresent(Insert::class.java) || method.isAnnotationPresent(Upsert::class.java) -> {
require(args != null)

val obj = instance()

method.parameters
.mapIndexed { index, value -> index to value }
.filter { (_, it) -> it.isAnnotationPresent(Parameter::class.java) }
.forEach { (index, param) ->
val name = param.getAnnotation(Parameter::class.java)!!.name.takeIf { it.isNotBlank() } ?: param.name

@Suppress("UNCHECKED_CAST")
val column = structure.getColumnFromCode(name) as DirectColumnData<T, Any>? ?: error("Column $name not found")
val value = args[index]

column.set(obj, value)
}

val result = if (method.isAnnotationPresent(Insert::class.java)) insert(obj) else upsert(obj)
return when {
method.returnType == UpdateResult::class.java -> result
else -> result.getOrThrow()
}
}

method.isAnnotationPresent(Update::class.java) -> {
require(args != null)

val updates = method.parameters
.mapIndexed { index, value -> index to value }
.filter { (_, it) -> it.isAnnotationPresent(Parameter::class.java) }
.map { (index, param) -> property<Any>(param.getAnnotation(Parameter::class.java)!!.name.takeIf { it.isNotBlank() } ?: param.name) to value(args[index]) }

val result = update(columns = updates.toTypedArray(), where = createCondition())
return when {
method.returnType == UpdateResult::class.java -> result
else -> result.getOrThrow()
}
}

method.isAnnotationPresent(Delete::class.java) -> return delete(where = createCondition())
}

return try {
javaClass.getMethod(method!!.name, *method.parameterTypes).invoke(this, *(args ?: emptyArray()))
javaClass.getMethod(method.name, *method.parameterTypes).invoke(this, *(args ?: emptyArray()))
} catch(_: NoSuchMethodException) {
type.java.classes.find { it.simpleName == "DefaultImpls" }?.getMethod(method!!.name, type.java, *method.parameterTypes)?.invoke(null, proxy, *(args ?: emptyArray()))
type.java.classes.find { it.simpleName == "DefaultImpls" }?.getMethod(method.name, type.java, *method.parameterTypes)?.invoke(null, proxy, *(args ?: emptyArray()))
} catch(e: IllegalAccessException) {
throw RuntimeException(e)
} catch(e: InvocationTargetException) {
Expand Down
7 changes: 7 additions & 0 deletions postgres/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ dependencies {

implementation(kotlin("reflect"))
implementation("com.google.code.gson:gson:2.10.1")
}

kotlin {
compilerOptions {
//Required for AnnotationTable
javaParameters = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import org.jdbi.v3.core.statement.Update
import org.postgresql.util.PSQLState
import java.sql.SQLException
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType

class PostgresTable<T: Any>(
Expand Down Expand Up @@ -210,21 +209,20 @@ class PostgresTable<T: Any>(
}
}

override fun <T> update(column: Node<T>, value: Node<T>, where: Where): UpdateResult<Int > {
val spec = column.columnContext(structure)!!
override fun update(vararg columns: Pair<Node<*>, Node<*>>, where: Where): UpdateResult<Int > {
val specs = columns.associate { (column, value) -> (column to column.columnContext(structure)!!) to (value to value.columnContext(structure)) }

require(spec.context.isEmpty()) { "Cannot update reference, update in the table directly" }
require(!spec.column.getRootColumn().key) { "Cannot update key" }
require(specs.all { (column) -> column.second.context.isEmpty() }) { "Cannot update reference, update in the table directly" }
require(specs.none { (column) -> column.second.column.getRootColumn().key }) { "Cannot update key" }

val sql = """
update ${ structure.name }
set ${ column.format(structure) { it.build(prefix = false) }} = ${ value.format(structure) }
set ${ specs.entries.joinToString { (column, value) -> "${ column.first.format(structure) { it.build(prefix = false) } } = ${ value.first.format(structure) }" } }
${ where.format(structure) }
""".trim().replace("\\s+".toRegex(), " ")

return createResult { structure.manager.execute { it.createUpdate(sql)
.bindMap(column.values(structure, spec.column))
.bindMap(value.values(structure, spec.column))
.bindMap(specs.flatMap { (column, value) -> column.first.values(structure, column.second.column).entries + value.first.values(structure, value.second?.column ?: column.second.column).entries }.associate { it.toPair() })
.bindMap(where.values(structure))
.execute()
} }
Expand Down
4 changes: 2 additions & 2 deletions postgres/src/test/kotlin/tests/postgres/general/Update.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ class UpdateTest {

@Test
fun updateName() {
assertEquals(1, table.update(UserDao::name, value("Test"), where = property(UserDao::id) isEqualTo value(1)).value)
assertEquals(1, table.update(property(UserDao::name) to value("Test"), where = property(UserDao::id) isEqualTo value(1)).value)
assertEquals("Test", table.selectValue(property(UserDao::name), where = property(UserDao::id) isEqualTo value(1)).first())
}

@Test
fun updateConflict() {
val result = table.update(UserDao::email, value("[email protected]"), where = property(UserDao::id) isEqualTo value(1))
val result = table.update(property(UserDao::email) to value("[email protected]"), where = property(UserDao::id) isEqualTo value(1))

assertTrue(result.isError())
assertTrue(result.uniqueViolation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class ReferenceTest {

@Test
fun updateReference() {
bookTable.update(BookDao::publisher, value(publisherB), where = property(BookDao::title) isEqualTo value("The Hobbit"))
bookTable.update(property(BookDao::publisher) to value(publisherB), where = property(BookDao::title) isEqualTo value("The Hobbit"))
assertEquals(publisherB, bookTable.selectValue(property(BookDao::publisher), where = property(BookDao::title) isEqualTo value("The Hobbit")).first())
}
}
2 changes: 1 addition & 1 deletion postgres/src/test/kotlin/tests/postgres/specific/Array.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class ArrayTest {

@Test
fun updateCondition() {
assertTrue(table.update(property(ArrayDao::stringList)[0], value("e")).isSuccess())
assertTrue(table.update(property(ArrayDao::stringList)[0] to value("e")).isSuccess())
assertEquals(2, table.selectRowCount(where = property(ArrayDao::stringList)[0] isEqualTo value("e")))
}
}
8 changes: 5 additions & 3 deletions postgres/src/test/kotlin/tests/postgres/specific/Null.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,21 @@ class NullTest {
}

@Test
@Suppress("UNCHECKED_CAST")
fun updateNullError() {
fun checkResult(result: UpdateResult<*>) {
assertTrue(result.isError())
assertTrue(result.notNullViolation)
}

checkResult(table.update(NullDao::name, value(null), where = property(NullDao::id) isEqualTo value(1)))
checkResult(table.update(NullDao::name, nullValue(), where = property(NullDao::id) isEqualTo value(1)))
//Updating to null values with nun-null types will actually cause compile-time type problems without the unchecked casts
checkResult(table.update(property(NullDao::name) to (value<String?>(null) as Node<String>), where = property(NullDao::id) isEqualTo value(1)))
checkResult(table.update(property(NullDao::name) to (nullValue<String>() as Node<String>), where = property(NullDao::id) isEqualTo value(1)))
}

@Test
fun updateNull() {
val result = table.update(NullDao::test, nullValue(), where = property(NullDao::id) isEqualTo value(1))
val result = table.update(property(NullDao::test) to nullValue(), where = property(NullDao::id) isEqualTo value(1))

assertTrue(result.isSuccess())
assertEquals(1, result.value)
Expand Down
Loading

0 comments on commit eb5b064

Please sign in to comment.