diff --git a/README.md b/README.md index 6f476d2..f537457 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,12 @@ project.afterEvaluate { - `eq`, `neq`, `lt`, `gt`, `lte`, `gte`, `in`, `notIn`, `between`, and `notBetween` - Supports `like` and `notLike` expressions in a null-safe way for String columns - Supports `and` and `or` expressions for building up a query -- Adds the `PanacheSingleResult` sealed class and `singleResultSafe()` extension function to return a single result without throwing exceptions. +- Adds the `PanacheSingleResult` sealed class and `singleResultSafe()` extension function to return a single result without throwing exceptions. - Allows you to handle no/multiple results with a `when (result) {...}` block instead of try-catching ### Code Generation - Generate `Column`s for non-transient and non-mapped fields in Panache entities - Generate query entry point extension functions for entities with Panache companion objects - - `where` to start building a SELECT queries, which can be chained to other Panache functions + - `where` to start building a SELECT/DELETE queries, which may be chained to other Panache functions - `update` with setters to bulk-update multiple rows at once - Single expression `updateAll` to update all rows without requiring a WHERE clause - Single expression `count`, `delete`, `find`, `stream`, `single`, `singleSafe`, and `multiple` diff --git a/src/main/kotlin/ch/icken/processor/ColumnType.kt b/src/main/kotlin/ch/icken/processor/ColumnType.kt index d8397f2..db65fbc 100644 --- a/src/main/kotlin/ch/icken/processor/ColumnType.kt +++ b/src/main/kotlin/ch/icken/processor/ColumnType.kt @@ -20,6 +20,16 @@ import kotlin.annotation.AnnotationRetention.SOURCE import kotlin.annotation.AnnotationTarget.PROPERTY import kotlin.reflect.KClass +/** + * Specifies the generic type of the generated [Column][ch.icken.query.Column] to match the type as used by Hibernate. + * + * The intended use case for this annotation is when the column's Kotlin type is different from your database. + * When using JPA's [@Convert][jakarta.persistence.Convert] annotation, Hibernate converts to and from the type as + * specified by the [@Converter][jakarta.persistence.Converter]. Panache uses this other type in its queries, + * so it needs to be known during [Column][ch.icken.query.Column] generation. + * + * @property type the generic type to be used by the generated [Column][ch.icken.query.Column] + */ @Retention(SOURCE) @Target(PROPERTY) annotation class ColumnType( diff --git a/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt b/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt index d4c7760..666c2b6 100644 --- a/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt +++ b/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt @@ -86,6 +86,7 @@ class PanacheCompanionBaseProcessor( //endregion //region where, and, or + //TODO kdoc val where = FunSpec.builder(FUNCTION_NAME_WHERE) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -95,6 +96,7 @@ class PanacheCompanionBaseProcessor( MemberName(QueryComponentClassName.packageName, FUNCTION_NAME_WHERE), columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_WHERE$classSimpleName")) + //TODO kdoc val and = FunSpec.builder(FUNCTION_NAME_AND) .addModifiers(KModifier.INLINE) .receiver(queryComponentType) @@ -102,6 +104,7 @@ class PanacheCompanionBaseProcessor( .returns(queryComponentType) .addStatement("return $FUNCTION_NAME_AND($PARAM_NAME_EXPRESSION(%T))", columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_AND$classSimpleName")) + //TODO kdoc val or = FunSpec.builder(FUNCTION_NAME_OR) .addModifiers(KModifier.INLINE) .receiver(queryComponentType) @@ -112,6 +115,7 @@ class PanacheCompanionBaseProcessor( //endregion //region count, delete, find, stream + //TODO kdoc val count = FunSpec.builder(FUNCTION_NAME_COUNT) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -120,6 +124,7 @@ class PanacheCompanionBaseProcessor( .addStatement("return $FUNCTION_NAME_WHERE($PARAM_NAME_EXPRESSION).$FUNCTION_NAME_COUNT()") .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_COUNT$classSimpleName")) + //TODO kdoc val delete = FunSpec.builder(FUNCTION_NAME_DELETE) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -129,6 +134,7 @@ class PanacheCompanionBaseProcessor( .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_DELETE$classSimpleName")) val findReturns = PanacheQueryClassName.plusParameter(className) + //TODO kdoc val find = FunSpec.builder(FUNCTION_NAME_FIND) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -136,6 +142,7 @@ class PanacheCompanionBaseProcessor( .returns(findReturns) .addStatement("return $FUNCTION_NAME_WHERE($PARAM_NAME_EXPRESSION).$FUNCTION_NAME_FIND()") .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_FIND$classSimpleName")) + //TODO kdoc val findSorted = FunSpec.builder(FUNCTION_NAME_FIND) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -146,6 +153,7 @@ class PanacheCompanionBaseProcessor( .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_FIND_SORTED$classSimpleName")) val streamReturns = StreamClassName.plusParameter(className) + //TODO kdoc val stream = FunSpec.builder(FUNCTION_NAME_STREAM) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -153,6 +161,7 @@ class PanacheCompanionBaseProcessor( .returns(streamReturns) .addStatement("return $FUNCTION_NAME_WHERE($PARAM_NAME_EXPRESSION).$FUNCTION_NAME_STREAM()") .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_STREAM$classSimpleName")) + //TODO kdoc val streamSorted = FunSpec.builder(FUNCTION_NAME_STREAM) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -164,6 +173,7 @@ class PanacheCompanionBaseProcessor( //endregion //region single, multiple + //TODO kdoc val single = FunSpec.builder(FUNCTION_NAME_SINGLE) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -171,6 +181,7 @@ class PanacheCompanionBaseProcessor( .returns(className) .addStatement("return $FUNCTION_NAME_WHERE($PARAM_NAME_EXPRESSION).single()") .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_SINGLE$classSimpleName")) + //TODO kdoc val singleSafe = FunSpec.builder(FUNCTION_NAME_SINGLE_SAFE) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -180,6 +191,7 @@ class PanacheCompanionBaseProcessor( .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_SINGLE_SAFE$classSimpleName")) val multipleReturns = ListClassName.plusParameter(className) + //TODO kdoc val multiple = FunSpec.builder(FUNCTION_NAME_MULTIPLE) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -187,6 +199,7 @@ class PanacheCompanionBaseProcessor( .returns(multipleReturns) .addStatement("return $FUNCTION_NAME_WHERE($PARAM_NAME_EXPRESSION).multiple()") .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_MULTIPLE$classSimpleName")) + //TODO kdoc val multipleSorted = FunSpec.builder(FUNCTION_NAME_MULTIPLE) .addModifiers(KModifier.INLINE) .receiver(companionClassName) @@ -199,12 +212,14 @@ class PanacheCompanionBaseProcessor( //region update, updateAll val updateExtensionFunction = MemberName(InitialUpdateComponentClassName.packageName, FUNCTION_NAME_UPDATE) + //TODO kdoc val update = FunSpec.builder(FUNCTION_NAME_UPDATE) .receiver(companionClassName) .addParameter(PARAM_NAME_SETTER, setterExpressionParameterLambdaType) .returns(initialUpdateComponentType) .addStatement("return %M(%T, $PARAM_NAME_SETTER)", updateExtensionFunction, columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_UPDATE$classSimpleName")) + //TODO kdoc val updateMultiple = FunSpec.builder(FUNCTION_NAME_UPDATE) .receiver(companionClassName) .addParameter(PARAM_NAME_SETTERS, setterExpressionParameterLambdaType, KModifier.VARARG) @@ -212,6 +227,7 @@ class PanacheCompanionBaseProcessor( .addStatement("return %M(%T, $PARAM_NAME_SETTERS)", updateExtensionFunction, columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_UPDATE_MULTIPLE$classSimpleName")) + //TODO kdoc val updateAll = FunSpec.builder(FUNCTION_NAME_UPDATE_ALL) .receiver(companionClassName) .addParameter(PARAM_NAME_SETTER, setterExpressionParameterLambdaType) @@ -219,6 +235,7 @@ class PanacheCompanionBaseProcessor( .addStatement("return %M(%T, $PARAM_NAME_SETTER).executeWithoutWhere()", updateExtensionFunction, columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_UPDATE_ALL$classSimpleName")) + //TODO kdoc val updateAllMultiple = FunSpec.builder(FUNCTION_NAME_UPDATE_ALL) .receiver(companionClassName) .addParameter(PARAM_NAME_SETTERS, setterExpressionParameterLambdaType, KModifier.VARARG) @@ -229,6 +246,7 @@ class PanacheCompanionBaseProcessor( //endregion //region whereUpdate, andUpdate, orUpdate + //TODO kdoc val whereUpdate = FunSpec.builder(FUNCTION_NAME_WHERE) .addModifiers(KModifier.INLINE) .receiver(initialUpdateComponentType) @@ -237,6 +255,7 @@ class PanacheCompanionBaseProcessor( .addStatement("return $FUNCTION_NAME_WHERE($PARAM_NAME_EXPRESSION(%T))", columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_WHERE_UPDATE$classSimpleName")) + //TODO kdoc val andUpdate = FunSpec.builder(FUNCTION_NAME_AND) .addModifiers(KModifier.INLINE) .receiver(logicalUpdateComponentType) @@ -244,6 +263,7 @@ class PanacheCompanionBaseProcessor( .returns(logicalUpdateComponentType) .addStatement("return $FUNCTION_NAME_AND($PARAM_NAME_EXPRESSION(%T))", columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_AND_UPDATE$classSimpleName")) + //TODO kdoc val orUpdate = FunSpec.builder(FUNCTION_NAME_OR) .addModifiers(KModifier.INLINE) .receiver(logicalUpdateComponentType) @@ -254,6 +274,7 @@ class PanacheCompanionBaseProcessor( //endregion //region andExpression, orExpression + //TODO kdoc val andExpression = FunSpec.builder(FUNCTION_NAME_AND) .addModifiers(KModifier.INLINE) .receiver(expressionType) @@ -261,6 +282,7 @@ class PanacheCompanionBaseProcessor( .returns(expressionType) .addStatement("return $FUNCTION_NAME_AND($PARAM_NAME_EXPRESSION(%T))", columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_AND_EXPRESSION$classSimpleName")) + //TODO kdoc val orExpression = FunSpec.builder(FUNCTION_NAME_OR) .addModifiers(KModifier.INLINE) .receiver(expressionType) diff --git a/src/main/kotlin/ch/icken/query/Column.kt b/src/main/kotlin/ch/icken/query/Column.kt index 466b6ee..4889c24 100644 --- a/src/main/kotlin/ch/icken/query/Column.kt +++ b/src/main/kotlin/ch/icken/query/Column.kt @@ -24,85 +24,156 @@ import ch.icken.query.Expression.BooleanExpression.BooleanValueExpression.* import ch.icken.query.Expression.BooleanExpression.IsExpression.IsNotNull import ch.icken.query.Expression.BooleanExpression.IsExpression.IsNull -class Column(internal val name: String) { - operator fun invoke(value: T) = Component.UpdateComponent.InitialUpdateComponent.Setter(name, value) +class Column(internal val name: String) { + /** + * Adds a setter expression to this update query + * + * @param value the new value for this column + */ + infix fun set(value: Type) = Component.UpdateComponent.InitialUpdateComponent.Setter(name, value) } //region eq private fun eq(name: String, value: Any?): Expression = if (value == null) IsNull(name) else EqualTo(name, value) +/** + * TODO + */ @JvmName("eq") -infix fun Column.eq(value: T) = eq(name, value) +infix fun Column.eq(value: Type) = eq(name, value) +/** + * TODO + */ @JvmName("eqNullable") -infix fun Column.eq(value: T?) = eq(name, value) +infix fun Column.eq(value: Type?) = eq(name, value) //endregion //region neq private fun neq(name: String, value: Any?): Expression = if (value == null) IsNotNull(name) else NotEqualTo(name, value) +/** + * TODO + */ @JvmName("neq") -infix fun Column.neq(value: T) = neq(name, value) +infix fun Column.neq(value: Type) = neq(name, value) +/** + * TODO + */ @JvmName("neqNullable") -infix fun Column.neq(value: T?) = neq(name, value) +infix fun Column.neq(value: Type?) = neq(name, value) //endregion //region lt private fun lt(name: String, value: Any): Expression = LessThan(name, value) +/** + * TODO + */ @JvmName("lt") -infix fun Column.lt(value: T) = lt(name, value) +infix fun Column.lt(value: Type) = lt(name, value) +/** + * TODO + */ @JvmName("ltNullable") -infix fun Column.lt(value: T) = lt(name, value) +infix fun Column.lt(value: Type) = lt(name, value) //endregion //region gt private fun gt(name: String, value: Any): Expression = GreaterThan(name, value) +/** + * TODO + */ @JvmName("gt") -infix fun Column.gt(value: T) = gt(name, value) +infix fun Column.gt(value: Type) = gt(name, value) +/** + * TODO + */ @JvmName("gtNullable") -infix fun Column.gt(value: T) = gt(name, value) +infix fun Column.gt(value: Type) = gt(name, value) //endregion //region lte private fun lte(name: String, value: Any): Expression = LessThanOrEqualTo(name, value) +/** + * TODO + */ @JvmName("lte") -infix fun Column.lte(value: T) = lte(name, value) +infix fun Column.lte(value: Type) = lte(name, value) +/** + * TODO + */ @JvmName("lteNullable") -infix fun Column.lte(value: T) = lte(name, value) +infix fun Column.lte(value: Type) = lte(name, value) //endregion //region gte private fun gte(name: String, value: Any): Expression = GreaterThanOrEqualTo(name, value) +/** + * TODO + */ @JvmName("gte") -infix fun Column.gte(value: T) = gte(name, value) +infix fun Column.gte(value: Type) = gte(name, value) +/** + * TODO + */ @JvmName("gteNullable") -infix fun Column.gte(value: T) = gte(name, value) +infix fun Column.gte(value: Type) = gte(name, value) //endregion //region in private fun `in`(name: String, values: Collection): Expression = In(name, values) +/** + * TODO + */ @JvmName("in") -infix fun Column.`in`(values: Collection) = `in`(name, values) +infix fun Column.`in`(values: Collection) = `in`(name, values) +/** + * TODO + */ @JvmName("inNullable") -infix fun Column.`in`(values: Collection) = `in`(name, values) +infix fun Column.`in`(values: Collection) = `in`(name, values) //endregion //region notIn private fun notIn(name: String, values: Collection): Expression = NotIn(name, values) +/** + * TODO + */ @JvmName("notIn") -infix fun Column.notIn(values: Collection) = notIn(name, values) +infix fun Column.notIn(values: Collection) = notIn(name, values) +/** + * TODO + */ @JvmName("notInNullable") -infix fun Column.notIn(values: Collection) = notIn(name, values) +infix fun Column.notIn(values: Collection) = notIn(name, values) //endregion //region like private fun like(name: String, expression: String?): Expression = if (expression == null) IsNull(name) else Like(name, expression) +/** + * TODO + */ @JvmName("like") infix fun Column.like(expression: String) = like(name, expression) +/** + * TODO + */ @JvmName("likeNullable") infix fun Column.like(expression: String?) = like(name, expression) +//TODO startsWith +//TODO contains +//TODO endsWith //endregion //region notLike private fun notLike(name: String, expression: String?): Expression = if (expression == null) IsNotNull(name) else NotLike(name, expression) +/** + * TODO + */ @JvmName("notLike") infix fun Column.notLike(expression: String) = notLike(name, expression) +/** + * TODO + */ @JvmName("notLikeNullable") infix fun Column.notLike(expression: String?) = notLike(name, expression) +//TODO notStartsWith +//TODO notContains +//TODO notEndsWith //endregion //region between @@ -112,10 +183,18 @@ private fun between(name: String, min: Any?, maxIncl: Any?): Expressio min == null && maxIncl != null -> LessThanOrEqualTo(name, maxIncl) else -> IsNull(name) } +/** + * TODO + */ @JvmName("between") -fun Column.between(min: T, maxIncl: T) = between(name, min, maxIncl) +fun Column.between(min: Type, maxIncl: Type) = + between(name, min, maxIncl) +/** + * TODO + */ @JvmName("betweenNullable") -fun Column.between(min: T?, maxIncl: T?) = between(name, min, maxIncl) +fun Column.between(min: Type?, maxIncl: Type?) = + between(name, min, maxIncl) //endregion //region notBetween private fun notBetween(name: String, min: Any?, maxIncl: Any?): Expression = when { @@ -124,8 +203,16 @@ private fun notBetween(name: String, min: Any?, maxIncl: Any?): Expres min == null && maxIncl != null -> GreaterThan(name, maxIncl) else -> IsNotNull(name) } +/** + * TODO + */ @JvmName("notBetween") -fun Column.notBetween(min: T, maxIncl: T) = notBetween(name, min, maxIncl) +fun Column.notBetween(min: Type, maxIncl: Type) = + notBetween(name, min, maxIncl) +/** + * TODO + */ @JvmName("notBetweenNullable") -fun Column.notBetween(min: T?, maxIncl: T?) = notBetween(name, min, maxIncl) +fun Column.notBetween(min: Type?, maxIncl: Type?) = + notBetween(name, min, maxIncl) //endregion diff --git a/src/main/kotlin/ch/icken/query/Component.kt b/src/main/kotlin/ch/icken/query/Component.kt index 093b81d..7c9c8ab 100644 --- a/src/main/kotlin/ch/icken/query/Component.kt +++ b/src/main/kotlin/ch/icken/query/Component.kt @@ -50,23 +50,59 @@ sealed class Component private co protected val expression: Expression ) : Component(companion) { //region Chaining operations + /** + * TODO + */ fun and(expression: Expression): QueryComponent = LogicalQueryComponent.AndQueryComponent(companion, this, expression) + /** + * TODO + */ fun or(expression: Expression): QueryComponent = LogicalQueryComponent.OrQueryComponent(companion, this, expression) //endregion //region Terminal operations + /** + * TODO + */ fun count() = withCompiled { companion.count(component, parameters) } + /** + * TODO + */ fun delete() = withCompiled { companion.delete(component, parameters) } + /** + * TODO + */ fun find() = withCompiled { companion.find(component, parameters) } + /** + * TODO + */ fun find(sort: Sort) = withCompiled { companion.find(component, sort, parameters) } + /** + * TODO + */ fun stream() = withCompiled { companion.stream(component, parameters) } + /** + * TODO + */ fun stream(sort: Sort) = withCompiled { companion.stream(component, sort, parameters) } + /** + * TODO + */ fun single() = find().singleResult() + /** + * TODO + */ fun singleSafe() = find().singleResultSafe() + /** + * TODO + */ fun multiple() = find().list() + /** + * TODO + */ fun multiple(sort: Sort) = find(sort).list() //endregion @@ -116,11 +152,17 @@ sealed class Component private co private val setters: Array Setter> ) : UpdateComponent(companion) { //region Chaining operations + /** + * TODO + */ fun where(expression: Expression): LogicalUpdateComponent = LogicalUpdateComponent.WhereUpdateComponent(companion, this, expression) //endregion //region Terminal operations + /** + * TODO + */ fun executeWithoutWhere() = withCompiled { companion.update(component, parameters) } //endregion @@ -150,13 +192,22 @@ sealed class Component private co private val expression: Expression ) : UpdateComponent(companion) { //region Chaining operations + /** + * TODO + */ fun and(expression: Expression): LogicalUpdateComponent = AndUpdateComponent(companion, this, expression) + /** + * TODO + */ fun or(expression: Expression): LogicalUpdateComponent = OrUpdateComponent(companion, this, expression) //endregion //region Terminal operations + /** + * TODO + */ fun execute() = withCompiled { companion.update(component, parameters) } //endregion diff --git a/src/main/kotlin/ch/icken/query/PanacheSingleResult.kt b/src/main/kotlin/ch/icken/query/PanacheSingleResult.kt index 85f9b7e..8fa789e 100644 --- a/src/main/kotlin/ch/icken/query/PanacheSingleResult.kt +++ b/src/main/kotlin/ch/icken/query/PanacheSingleResult.kt @@ -18,7 +18,7 @@ package ch.icken.query import io.quarkus.hibernate.orm.panache.kotlin.PanacheQuery -sealed class PanacheSingleResult { +sealed class PanacheSingleResult { data object NoResult : PanacheSingleResult() data class Result(val value: Entity) : PanacheSingleResult() data object NotUnique : PanacheSingleResult() diff --git a/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt b/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt index e457ae8..5ab289f 100644 --- a/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt +++ b/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt @@ -40,7 +40,7 @@ class UpdateTests { // When //UPDATE EMPLOYEE SET SALARY = 0.0 - val entitiesUpdated = Employee.updateAll { salary(0.0) } + val entitiesUpdated = Employee.updateAll { salary set 0.0 } // Then assertEquals(7, entitiesUpdated) @@ -57,7 +57,7 @@ class UpdateTests { // When //UPDATE EMPLOYEE SET SALARY = 100000.0 WHERE SALARY < 100000.0 val entitiesUpdated = Employee - .update { salary(100_000.0) } + .update { salary set 100_000.0 } .where { salary lt 100_000.0 } .execute()