From 064281862b3684ca34c7ea4c535ef8566fd5cd76 Mon Sep 17 00:00:00 2001 From: Jake Son Date: Sun, 2 Nov 2025 17:31:35 +0900 Subject: [PATCH] Fix KSP type extraction for Comparable and Number types with @Convert Remove annotation-based type detection that prevented proper type hierarchy analysis. Types implementing Comparable or extending Number now correctly generate ComparablePath and NumberPath respectively, matching Java APT behavior. - Remove userType() method that checked for @Convert, @Type, @JdbcTypeCode - Enhance fallbackType() to detect Comparable and Number via type hierarchy - Add test cases for YearMonth (Comparable) and Money (Number+Comparable) --- .../com/querydsl/example/ksp/Invoice.kt | 23 +++++ .../kotlin/com/querydsl/example/ksp/Money.kt | 16 ++++ .../querydsl/example/ksp/MoneyConverter.kt | 18 ++++ .../example/ksp/YearMonthConverter.kt | 18 ++++ .../src/test/kotlin/Tests.kt | 13 +++ .../ksp/codegen/QueryModelExtractor.kt | 2 - .../com/querydsl/ksp/codegen/TypeExtractor.kt | 87 ++++++++++--------- 7 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Invoice.kt create mode 100644 querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Money.kt create mode 100644 querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/MoneyConverter.kt create mode 100644 querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/YearMonthConverter.kt diff --git a/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Invoice.kt b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Invoice.kt new file mode 100644 index 000000000..468fa13aa --- /dev/null +++ b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Invoice.kt @@ -0,0 +1,23 @@ +package com.querydsl.example.ksp + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.Id +import java.time.YearMonth +import java.util.UUID + +@Entity +class Invoice( + @Id + val id: UUID, + + @Column(nullable = false, length = 7) + @Convert(converter = YearMonthConverter::class) + val month: YearMonth?, + + @Column(nullable = false, precision = 30, scale = 10) + @Convert(converter = MoneyConverter::class) + val amount: Money?, +) + diff --git a/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Money.kt b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Money.kt new file mode 100644 index 000000000..ccec1181f --- /dev/null +++ b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/Money.kt @@ -0,0 +1,16 @@ +package com.querydsl.example.ksp + +import java.math.BigDecimal + +data class Money(val value: BigDecimal) : Number(), Comparable { + + override fun toByte(): Byte = value.toByte() + override fun toDouble(): Double = value.toDouble() + override fun toFloat(): Float = value.toFloat() + override fun toInt(): Int = value.toInt() + override fun toLong(): Long = value.toLong() + override fun toShort(): Short = value.toShort() + + override fun compareTo(other: Money): Int = this.value.compareTo(other.value) +} + diff --git a/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/MoneyConverter.kt b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/MoneyConverter.kt new file mode 100644 index 000000000..5fa89f0f0 --- /dev/null +++ b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/MoneyConverter.kt @@ -0,0 +1,18 @@ +package com.querydsl.example.ksp + +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import java.math.BigDecimal + +@Converter +class MoneyConverter : AttributeConverter { + + override fun convertToDatabaseColumn(attribute: Money?): BigDecimal? { + return attribute?.value + } + + override fun convertToEntityAttribute(dbData: BigDecimal?): Money? { + return dbData?.let { Money(it) } + } +} + diff --git a/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/YearMonthConverter.kt b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/YearMonthConverter.kt new file mode 100644 index 000000000..b65763de5 --- /dev/null +++ b/querydsl-examples/querydsl-example-ksp-codegen/src/main/kotlin/com/querydsl/example/ksp/YearMonthConverter.kt @@ -0,0 +1,18 @@ +package com.querydsl.example.ksp + +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import java.time.YearMonth + +@Converter +class YearMonthConverter : AttributeConverter { + + override fun convertToDatabaseColumn(attribute: YearMonth?): String? { + return attribute?.toString() + } + + override fun convertToEntityAttribute(dbData: String?): YearMonth? { + return dbData?.let { YearMonth.parse(it) } + } +} + diff --git a/querydsl-examples/querydsl-example-ksp-codegen/src/test/kotlin/Tests.kt b/querydsl-examples/querydsl-example-ksp-codegen/src/test/kotlin/Tests.kt index f55cadb1b..2a0c45888 100644 --- a/querydsl-examples/querydsl-example-ksp-codegen/src/test/kotlin/Tests.kt +++ b/querydsl-examples/querydsl-example-ksp-codegen/src/test/kotlin/Tests.kt @@ -9,6 +9,7 @@ import com.querydsl.example.ksp.QBear import com.querydsl.example.ksp.QBearSimplifiedProjection import com.querydsl.example.ksp.QCat import com.querydsl.example.ksp.QDog +import com.querydsl.example.ksp.QInvoice import com.querydsl.example.ksp.QMyShape import com.querydsl.example.ksp.QPerson import com.querydsl.example.ksp.QPersonClassDTO @@ -290,6 +291,18 @@ class Tests { assertThat(departureProperty.returnType.arguments.single().type!!.jvmErasure.qualifiedName!!).isEqualTo("org.locationtech.jts.geom.Geometry") } + @Test + fun `Invoice month and amount generate correct path types`() { + val monthProperty = QInvoice::class.memberProperties.single { it.name == "month" } + val amountProperty = QInvoice::class.memberProperties.single { it.name == "amount" } + + assertThat(monthProperty.returnType.jvmErasure.qualifiedName!!).isEqualTo("com.querydsl.core.types.dsl.ComparablePath") + assertThat(monthProperty.returnType.arguments.single().type!!.jvmErasure.qualifiedName!!).isEqualTo("java.time.YearMonth") + + assertThat(amountProperty.returnType.jvmErasure.qualifiedName!!).isEqualTo("com.querydsl.core.types.dsl.NumberPath") + assertThat(amountProperty.returnType.arguments.single().type!!.jvmErasure.qualifiedName!!).isEqualTo("com.querydsl.example.ksp.Money") + } + private fun initialize(): EntityManagerFactory { val configuration = Configuration() .setProperty(AvailableSettings.JAKARTA_JDBC_DRIVER, org.h2.Driver::class.qualifiedName!!) diff --git a/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryModelExtractor.kt b/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryModelExtractor.kt index 071facea9..5cfe50337 100644 --- a/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryModelExtractor.kt +++ b/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryModelExtractor.kt @@ -90,7 +90,6 @@ class QueryModelExtractor( val extractor = TypeExtractor( settings, "${declaration.parent?.location} - $paramName", - parameter.annotations ) val type = extractor.extract(parameter.type.resolve()) QProperty(paramName, type) @@ -109,7 +108,6 @@ class QueryModelExtractor( val extractor = TypeExtractor( settings, property.simpleName.asString(), - property.annotations ) val type = extractor.extract(property.type.resolve()) QProperty(propName, type) diff --git a/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/TypeExtractor.kt b/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/TypeExtractor.kt index 23e6722f9..639be4958 100644 --- a/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/TypeExtractor.kt +++ b/querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/TypeExtractor.kt @@ -6,12 +6,10 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName -import jakarta.persistence.Convert class TypeExtractor( private val settings: KspSettings, private val fullPathName: String, - private val annotations: Sequence ) { fun extract(type: KSType): QPropertyType { when (val declaration = type.declaration) { @@ -31,8 +29,7 @@ class TypeExtractor( return extract(innerType) } else -> { - return userType(type) - ?: parameterType(type) + return parameterType(type) ?: simpleType(type) ?: referenceType(type) ?: collectionType(type) @@ -58,24 +55,21 @@ class TypeExtractor( } } - private fun fallbackType(type: KSType): QPropertyType.Simple { + private fun fallbackType(type: KSType): QPropertyType { val declaration = type.declaration - val isComparable = if (declaration is KSClassDeclaration) { - val comparableNames = listOfNotNull( - Comparable::class.java.canonicalName, - java.lang.Comparable::class.qualifiedName - ) - declaration.getAllSuperTypes().any { - comparableNames.contains(it.toClassName().canonicalName) - } - } else { - false - } val className = type.toClassNameSimple() - if (isComparable) { - return QPropertyType.Simple(SimpleType.Comparable(className)) - } else { - return QPropertyType.Simple(SimpleType.Simple(className)) + + if (declaration !is KSClassDeclaration) { + return QPropertyType.Unknown(className, type.toTypeName()) + } + + val isComparable = declaration.isComparable() + val isNumber = declaration.isNumber() + + return when { + isComparable && isNumber -> QPropertyType.Simple(SimpleType.QNumber(className)) + isComparable -> QPropertyType.Simple(SimpleType.Comparable(className)) + else -> QPropertyType.Unknown(className, type.toTypeName()) } } @@ -144,23 +138,6 @@ class TypeExtractor( return QueryModelType.autodetect(classDeclaration) != null } - private fun userType(type: KSType): QPropertyType.Unknown? { - if (type.isEnum()) { - return null - } - - val userTypeAnnotations = listOf( - ClassName("org.hibernate.annotations", "Type"), - ClassName("org.hibernate.annotations", "JdbcTypeCode"), - Convert::class.asClassName() - ) - if (annotations.any { userTypeAnnotations.contains(it.annotationType.resolve().toClassName()) }) { - return QPropertyType.Unknown(type.toClassNameSimple(), type.toTypeName()) - } else { - return null - } - } - private fun assertTypeArgCount(parentType: KSType, collectionTypeName: String, count: Int) { if (parentType.arguments.size != count) { throwError("Type looks like a $collectionTypeName so expected $count type arguments, but got ${parentType.arguments.size}") @@ -209,8 +186,38 @@ private fun KSType.toClassNameSimple(): ClassName { } } -private fun KSType.isEnum(): Boolean { - val referencedDeclaration = declaration +private fun KSClassDeclaration.hasSuperType(predicate: (ClassName) -> Boolean): Boolean { + return getAllSuperTypes().any { superType -> + val superDeclaration = superType.declaration + if (superDeclaration is KSClassDeclaration) { + predicate(superDeclaration.toClassName()) + } else { + false + } + } +} + +private fun KSClassDeclaration.isComparable(): Boolean { + val comparableNames = listOfNotNull( + Comparable::class.java.canonicalName, + java.lang.Comparable::class.qualifiedName + ) + return hasSuperType { className -> + comparableNames.contains(className.canonicalName) + } +} - return referencedDeclaration is KSClassDeclaration && referencedDeclaration.classKind == ClassKind.ENUM_CLASS +private fun KSClassDeclaration.isNumber(): Boolean { + val numberQualifiedNames = listOf( + "kotlin.Number", + "java.lang.Number" + ) + + if (qualifiedName?.asString() in numberQualifiedNames) { + return true + } + + return getAllSuperTypes().any { superType -> + superType.declaration.qualifiedName?.asString() in numberQualifiedNames + } }