From afed50d182c7c4304824cb187b9fae5ac7e643af Mon Sep 17 00:00:00 2001 From: Thijs Koppen Date: Tue, 12 Nov 2024 23:27:31 +0100 Subject: [PATCH] Add updateAll and update DSL UT --- .github/workflows/test.yaml | 4 +- CHANGELOG.md | 6 ++ README.md | 1 + .../PanacheCompanionBaseProcessor.kt | 24 ++++++- .../ch/icken/processor/ProcessorCommon.kt | 3 + ...nacheCompanionBaseProcessorCompileTests.kt | 4 +- .../test/kotlin/ch/icken/model/QueryTests.kt | 61 +++++++++++------ .../test/kotlin/ch/icken/model/UpdateTests.kt | 68 +++++++++++++++++++ 8 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 tests/src/test/kotlin/ch/icken/model/UpdateTests.kt diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d56b3d3..e349c80 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,8 +17,8 @@ jobs: with: distribution: 'temurin' java-version: '17' - - run: ./gradlew koverXmlReport --no-daemon --continue + - run: ./gradlew koverXmlReport --no-daemon - env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: ./gradlew sonar --no-daemon --info + run: ./gradlew sonar --no-daemon diff --git a/CHANGELOG.md b/CHANGELOG.md index 912880f..66a3dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.0.4 +- Add DSL for bulk updates +- Add printing of the Panache query at DEBUG log level + - **Note:** output does not follow a specific format like SQL, JPQL, or HQL, and should only be used in debugging + Use Hibernate's SQL logging functionality to see what is actually being queried + ## 0.0.3 - Remove `whereGroup`, `andGroup`, and `orGroup` functions in favor of expression chaining diff --git a/README.md b/README.md index e6d3559..6f476d2 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ project.afterEvaluate { - 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 - `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` - Allows for overriding the generated `Column`'s type parameter using `@ColumnType` - Especially useful when using a JPA `@Converter` when the field's type is different to the column's type diff --git a/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt b/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt index 53d7086..d4c7760 100644 --- a/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt +++ b/src/main/kotlin/ch/icken/processor/PanacheCompanionBaseProcessor.kt @@ -51,6 +51,7 @@ class PanacheCompanionBaseProcessor( val extensionFileName = classSimpleName + SUFFIX_FILE_EXTENSIONS logger.info("Generating $packageName.$extensionFileName") + //region Names and types val className = ksClass.toClassName() val columnsObjectClassName = ClassName(packageName, classSimpleName + SUFFIX_OBJECT_COLUMNS) val companionClassName = className.nestedClass(CLASS_NAME_COMPANION) @@ -82,6 +83,7 @@ class PanacheCompanionBaseProcessor( .plusParameter(className) .plusParameter(idClassName) .plusParameter(columnsObjectClassName) + //endregion //region where, and, or val where = FunSpec.builder(FUNCTION_NAME_WHERE) @@ -195,7 +197,7 @@ class PanacheCompanionBaseProcessor( .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_MULTIPLE_SORTED$classSimpleName")) //endregion - //region update, whereUpdate, andUpdate, orUpdate + //region update, updateAll val updateExtensionFunction = MemberName(InitialUpdateComponentClassName.packageName, FUNCTION_NAME_UPDATE) val update = FunSpec.builder(FUNCTION_NAME_UPDATE) .receiver(companionClassName) @@ -210,6 +212,23 @@ class PanacheCompanionBaseProcessor( .addStatement("return %M(%T, $PARAM_NAME_SETTERS)", updateExtensionFunction, columnsObjectClassName) .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_UPDATE_MULTIPLE$classSimpleName")) + val updateAll = FunSpec.builder(FUNCTION_NAME_UPDATE_ALL) + .receiver(companionClassName) + .addParameter(PARAM_NAME_SETTER, setterExpressionParameterLambdaType) + .returns(IntClassName) + .addStatement("return %M(%T, $PARAM_NAME_SETTER).executeWithoutWhere()", + updateExtensionFunction, columnsObjectClassName) + .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_UPDATE_ALL$classSimpleName")) + val updateAllMultiple = FunSpec.builder(FUNCTION_NAME_UPDATE_ALL) + .receiver(companionClassName) + .addParameter(PARAM_NAME_SETTERS, setterExpressionParameterLambdaType, KModifier.VARARG) + .returns(IntClassName) + .addStatement("return %M(%T, $PARAM_NAME_SETTERS).executeWithoutWhere()", + updateExtensionFunction, columnsObjectClassName) + .addAnnotation(jvmNameAnnotation("$FUNCTION_NAME_UPDATE_ALL_MULTIPLE$classSimpleName")) + //endregion + + //region whereUpdate, andUpdate, orUpdate val whereUpdate = FunSpec.builder(FUNCTION_NAME_WHERE) .addModifiers(KModifier.INLINE) .receiver(initialUpdateComponentType) @@ -254,7 +273,8 @@ class PanacheCompanionBaseProcessor( val functions = listOf(where, and, or, count, delete, find, findSorted, stream, streamSorted, single, singleSafe, multiple, multipleSorted, - update, updateMultiple, whereUpdate, andUpdate, orUpdate, + update, updateMultiple, updateAll, updateAllMultiple, + whereUpdate, andUpdate, orUpdate, andExpression, orExpression) FileSpec.builder(packageName, extensionFileName) diff --git a/src/main/kotlin/ch/icken/processor/ProcessorCommon.kt b/src/main/kotlin/ch/icken/processor/ProcessorCommon.kt index 540955a..7bad316 100644 --- a/src/main/kotlin/ch/icken/processor/ProcessorCommon.kt +++ b/src/main/kotlin/ch/icken/processor/ProcessorCommon.kt @@ -60,6 +60,7 @@ abstract class ProcessorCommon(options: Map) { internal val ExpressionClassName = Expression::class.asClassName() internal val GeneratedClassName = Generated::class.asClassName() internal val InitialUpdateComponentClassName = UpdateComponent.InitialUpdateComponent::class.asClassName() + internal val IntClassName = Int::class.asClassName() internal val JvmNameClassName = JvmName::class.asClassName() internal val ListClassName = List::class.asClassName() internal val LogicalUpdateComponentClassName = UpdateComponent.LogicalUpdateComponent::class.asClassName() @@ -92,6 +93,8 @@ abstract class ProcessorCommon(options: Map) { internal const val FUNCTION_NAME_STREAM = "stream" internal const val FUNCTION_NAME_STREAM_SORTED = "streamSorted" internal const val FUNCTION_NAME_UPDATE = "update" + internal const val FUNCTION_NAME_UPDATE_ALL = "updateAll" + internal const val FUNCTION_NAME_UPDATE_ALL_MULTIPLE = "updateAllMultiple" internal const val FUNCTION_NAME_UPDATE_MULTIPLE = "updateMultiple" internal const val FUNCTION_NAME_WHERE = "where" internal const val FUNCTION_NAME_WHERE_UPDATE = "whereUpdate" diff --git a/src/test/kotlin/ch/icken/processor/PanacheCompanionBaseProcessorCompileTests.kt b/src/test/kotlin/ch/icken/processor/PanacheCompanionBaseProcessorCompileTests.kt index a721988..b6f5a71 100644 --- a/src/test/kotlin/ch/icken/processor/PanacheCompanionBaseProcessorCompileTests.kt +++ b/src/test/kotlin/ch/icken/processor/PanacheCompanionBaseProcessorCompileTests.kt @@ -47,7 +47,7 @@ class PanacheCompanionBaseProcessorCompileTests : ProcessorCompileTestCommon() { compilation.assertHasFile("EmployeeExtensions.kt") val employeeExtensions = result.loadClass("EmployeeExtensionsKt") - employeeExtensions.assertNumberOfDeclaredMethods(20) + employeeExtensions.assertNumberOfDeclaredMethods(22) employeeExtensions.assertHasDeclaredMethodWithName("andEmployee") employeeExtensions.assertHasDeclaredMethodWithName("andExpressionEmployee") employeeExtensions.assertHasDeclaredMethodWithName("andUpdateEmployee") @@ -64,6 +64,8 @@ class PanacheCompanionBaseProcessorCompileTests : ProcessorCompileTestCommon() { employeeExtensions.assertHasDeclaredMethodWithName("singleSafeEmployee") employeeExtensions.assertHasDeclaredMethodWithName("streamEmployee") employeeExtensions.assertHasDeclaredMethodWithName("streamSortedEmployee") + employeeExtensions.assertHasDeclaredMethodWithName("updateAllEmployee") + employeeExtensions.assertHasDeclaredMethodWithName("updateAllMultipleEmployee") employeeExtensions.assertHasDeclaredMethodWithName("updateEmployee") employeeExtensions.assertHasDeclaredMethodWithName("updateMultipleEmployee") employeeExtensions.assertHasDeclaredMethodWithName("whereEmployee") diff --git a/tests/src/test/kotlin/ch/icken/model/QueryTests.kt b/tests/src/test/kotlin/ch/icken/model/QueryTests.kt index 24e055b..25f53e1 100644 --- a/tests/src/test/kotlin/ch/icken/model/QueryTests.kt +++ b/tests/src/test/kotlin/ch/icken/model/QueryTests.kt @@ -28,49 +28,39 @@ import java.time.LocalDate class QueryTests { @Test - fun findJohn() { + fun testCount() { - //WHERE FIRST_NAME = 'John' - //Using type-safe sealed result wrapper - val john = Employee.where { firstName eq "John" }.singleSafe() - - assertInstanceOf(PanacheSingleResult.Result::class.java, john) - } - - @Test - fun countNotMen() { + // Given + // When //SELECT COUNT(*) FROM EMPLOYEE WHERE GENDER != 'M' val numberOfNotMen = Employee.count { gender neq Employee.Gender.M } + // Then assertEquals(4, numberOfNotMen) } @Test - fun bornBeforeEpoch() { + fun testFind() { - //WHERE BIRTH_DATE < 1970-01-01 - val bornBeforeEpoch = Employee.multiple { birthDate lt LocalDate.EPOCH } - - assertEquals(2, bornBeforeEpoch.size) - } - - @Test - fun findAllSons() { + // Given + // When //WHERE LAST_NAME LIKE '%son' - //Using find, which allows Panache pagination etc. to be used still val sons = Employee.find { lastName like "%son" }.list() + // Then assertEquals(2, sons.size) } @Test - fun averageSalary() { + fun testStream() { + + // Given + // When //WHERE (SALARY > 50000.0 AND SALARY <= 60000.0) // OR SALARY BETWEEN 75000.0 AND 85000.0 - //Then we take the average using Java 8 streams val averageSalary = Employee .where { salary.gt(50_000.0) @@ -82,6 +72,33 @@ class QueryTests { .average() .orElse(0.0) + // Then assertEquals(67_500.0, averageSalary) } + + @Test + fun testSingleSafe() { + + // Given + + // When + //WHERE FIRST_NAME = 'John' + val john = Employee.where { firstName eq "John" }.singleSafe() + + // Then + assertInstanceOf(PanacheSingleResult.Result::class.java, john) + } + + @Test + fun testMultiple() { + + // Given + + // When + //WHERE BIRTH_DATE < 1970-01-01 + val bornBeforeEpoch = Employee.multiple { birthDate lt LocalDate.EPOCH } + + // Then + assertEquals(2, bornBeforeEpoch.size) + } } diff --git a/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt b/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt new file mode 100644 index 0000000..e457ae8 --- /dev/null +++ b/tests/src/test/kotlin/ch/icken/model/UpdateTests.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Thijs Koppen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.icken.model + +import ch.icken.model.generated.count +import ch.icken.model.generated.update +import ch.icken.model.generated.updateAll +import ch.icken.model.generated.where +import ch.icken.query.eq +import ch.icken.query.gte +import ch.icken.query.lt +import io.quarkus.test.TestTransaction +import io.quarkus.test.junit.QuarkusTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@QuarkusTest +class UpdateTests { + + @Test + @TestTransaction + fun testUpdateAll() { + + // Given + + // When + //UPDATE EMPLOYEE SET SALARY = 0.0 + val entitiesUpdated = Employee.updateAll { salary(0.0) } + + // Then + assertEquals(7, entitiesUpdated) + assertEquals(7, Employee.count { salary eq 0.0 }) + assertTrue(Employee.listAll().all { it.salary == 0.0 }) + } + + @Test + @TestTransaction + fun testUpdateWhere() { + + // Given + + // When + //UPDATE EMPLOYEE SET SALARY = 100000.0 WHERE SALARY < 100000.0 + val entitiesUpdated = Employee + .update { salary(100_000.0) } + .where { salary lt 100_000.0 } + .execute() + + // Then + assertEquals(4, entitiesUpdated) + assertEquals(7, Employee.count { salary gte 100_000.0 }) + } +}