diff --git a/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts b/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts index bb46335d..62c25363 100644 --- a/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/ktorm.publish.gradle.kts @@ -155,6 +155,11 @@ publishing { id.set("brohacz") name.set("Michal Brosig") } + developer { + id.set("qumn") + name.set("qumn") + email.set("2476573497@qq.com") + } } } } diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/SuperTableClass.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/SuperTableClass.kt new file mode 100644 index 00000000..27d08798 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/SuperTableClass.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018-2024 the original author or authors. + * + * 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 org.ktorm.ksp.annotation + +import org.ktorm.schema.BaseTable +import kotlin.reflect.KClass + +/** + * Be used on Entity interface, to specify the super table class for the table class generated. + * @property value the super table class, which should be a subclass of `org.ktorm.schema.BaseTable` + * the `tableName` and `alias` parameters be required on primary constructor, other parameters are optional. + * + * if not specified, the super table class will be determined by the kind of the entity class. + * `org.ktorm.schema.Table` for interface, `org.ktorm.schema.BaseTable` for class. + * + * If there are multiple `SuperTableClass` on the inheritance hierarchy of entity, + * and they have an inheritance relationship, the super table class will be the last one of them. + * and they don't have an inheritance relationship, an error will be reported. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +public annotation class SuperTableClass( + public val value: KClass> +) diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/TableClassGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/TableClassGenerator.kt index 98609c1e..5a08c119 100644 --- a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/TableClassGenerator.kt +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/TableClassGenerator.kt @@ -28,9 +28,7 @@ import org.ktorm.ksp.compiler.util._type import org.ktorm.ksp.compiler.util.getKotlinType import org.ktorm.ksp.compiler.util.getRegisteringCodeBlock import org.ktorm.ksp.spi.TableMetadata -import org.ktorm.schema.BaseTable import org.ktorm.schema.Column -import org.ktorm.schema.Table @OptIn(KotlinPoetKspPreview::class) internal object TableClassGenerator { @@ -49,11 +47,7 @@ internal object TableClassGenerator { } private fun TypeSpec.Builder.configureSuperClass(table: TableMetadata): TypeSpec.Builder { - if (table.entityClass.classKind == ClassKind.INTERFACE) { - superclass(Table::class.asClassName().parameterizedBy(table.entityClass.toClassName())) - } else { - superclass(BaseTable::class.asClassName().parameterizedBy(table.entityClass.toClassName())) - } + superclass(table.superClass.parameterizedBy(table.entityClass.toClassName())) addSuperclassConstructorParameter("%S", table.name) addSuperclassConstructorParameter("alias") diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt index 55b94489..5dce0c5b 100644 --- a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt @@ -21,6 +21,10 @@ import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.* import com.google.devtools.ksp.symbol.ClassKind.* +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName import org.ktorm.entity.Entity import org.ktorm.ksp.annotation.* import org.ktorm.ksp.compiler.util.* @@ -93,6 +97,11 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn _logger.info("[ktorm-ksp-compiler] parse table metadata from entity: $className") val table = cls.getAnnotationsByType(Table::class).first() + + val (finalSuperClass, allSuperTableClasses) = parseSuperTableClass(cls) + val shouldIgnorePropertyNames = + allSuperTableClasses.flatMap { it.getProperties(emptySet()) }.map { it.simpleName.asString() } + val tableMetadata = TableMetadata( entityClass = cls, name = table.name.ifEmpty { _databaseNamingStrategy.getTableName(cls) }, @@ -101,8 +110,9 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn schema = table.schema.ifEmpty { _options["ktorm.schema"] }?.takeIf { it.isNotEmpty() }, tableClassName = table.className.ifEmpty { _codingNamingStrategy.getTableClassName(cls) }, entitySequenceName = table.entitySequenceName.ifEmpty { _codingNamingStrategy.getEntitySequenceName(cls) }, - ignoreProperties = table.ignoreProperties.toSet(), - columns = ArrayList() + ignoreProperties = table.ignoreProperties.toSet() + shouldIgnorePropertyNames, + columns = ArrayList(), + superClass = finalSuperClass ) val columns = tableMetadata.columns as MutableList @@ -196,8 +206,8 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn if (!hasConstructor) { val msg = "" + - "Parse sqlType error for property $propName: the sqlType should be a Kotlin singleton object or " + - "a normal class with a constructor that accepts a single org.ktorm.schema.TypeReference argument." + "Parse sqlType error for property $propName: the sqlType should be a Kotlin singleton object or " + + "a normal class with a constructor that accepts a single org.ktorm.schema.TypeReference argument." throw IllegalArgumentException(msg) } } @@ -274,6 +284,84 @@ internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEn ) } + /** + * @return the final super table class and all super table classes in the inheritance hierarchy. + */ + @OptIn(KotlinPoetKspPreview::class) + private fun parseSuperTableClass(cls: KSClassDeclaration): Pair> { + val entityAnnPairs = + cls.findAnnotationsInHierarchy(SuperTableClass::class.qualifiedName!!) + + // if there is no SuperTableClass annotation, return the default super table class based on the class kind. + if (entityAnnPairs.isEmpty()) { + return if (cls.classKind == INTERFACE) { + org.ktorm.schema.Table::class.asClassName() to emptySet() + } else { + org.ktorm.schema.BaseTable::class.asClassName() to emptySet() + } + } + + // SuperTableClass annotation can only be used on interface + if (entityAnnPairs.map { it.first }.any { it.classKind != INTERFACE }) { + val msg = "SuperTableClass annotation can only be used on interface." + throw IllegalArgumentException(msg) + } + + val superTableClasses = entityAnnPairs + .map { it.second } + .map { it.arguments.single { it.name?.asString() == SuperTableClass::value.name } } + .map { it.value as KSType } + .map { it.declaration as KSClassDeclaration } + + val lowestSubClass = findLowestSubClass(superTableClasses, cls) + + validateSuperTableClassConstructor(lowestSubClass) + + return lowestSubClass.toClassName() to superTableClasses.toSet() + } + + // find the last annotation in the inheritance hierarchy + private fun findLowestSubClass( + superTableClasses: List, + cls: KSClassDeclaration, + ): KSClassDeclaration { + var lowestSubClass = superTableClasses.first() + for (i in 1 until superTableClasses.size) { + val cur = superTableClasses[i] + if (cur.isSubclassOf(lowestSubClass)) { + lowestSubClass = cur + } else if (!lowestSubClass.isSubclassOf(cur)) { + val msg = """ + There are multiple SuperTableClass annotations in the inheritance hierarchy + of class ${cls.qualifiedName?.asString()}. + but the values of annotation are not in the same inheritance hierarchy. + """.trimIndent() + throw IllegalArgumentException(msg) + } + } + return lowestSubClass + } + + // validate the primary constructor of the super table class + private fun validateSuperTableClassConstructor(superTableClass: KSClassDeclaration) { + if (superTableClass.primaryConstructor == null) { + val msg = + "The super table class ${superTableClass.qualifiedName?.asString()} should have a primary constructor." + throw IllegalArgumentException(msg) + } + + val parameters = superTableClass.primaryConstructor!!.parameters + if (parameters.size < 2 || + parameters[0].name!!.asString() != "tableName" || + parameters[1].name!!.asString() != "alias" + ) { + val msg = "" + + "The super table class ${superTableClass.qualifiedName?.asString()} should have " + + "a primary constructor with parameters tableName and alias." + throw IllegalArgumentException(msg) + } + } + private fun TableMetadata.checkCircularRef(ref: KSClassDeclaration, stack: LinkedList = LinkedList()) { val className = this.entityClass.qualifiedName?.asString() val refClassName = ref.qualifiedName?.asString() diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/KspExtensions.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/KspExtensions.kt index fc96866a..3c161fed 100644 --- a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/KspExtensions.kt +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/KspExtensions.kt @@ -56,6 +56,13 @@ internal inline fun KSClassDeclaration.isSubclassOf(): Boolean return findSuperTypeReference(T::class.qualifiedName!!) != null } +/** + * Check if this class is a subclass of declaration. + */ +internal fun KSClassDeclaration.isSubclassOf(declaration: KSClassDeclaration): Boolean { + return findSuperTypeReference(declaration.qualifiedName!!.asString()) != null +} + /** * Find the specific super type reference for this class. */ @@ -76,6 +83,26 @@ internal fun KSClassDeclaration.findSuperTypeReference(name: String): KSTypeRefe return null } +/** + * Find all annotations with the given name in the inheritance hierarchy of this class. + * + * @param name the qualified name of the annotation. + * @return a list of pairs, each pair contains the class declaration and the annotation. + */ +internal fun KSClassDeclaration.findAnnotationsInHierarchy(name: String): List> { + val pairs = mutableListOf>() + + fun KSClassDeclaration.collectAnnotations() { + pairs += annotations + .filter { it.annotationType.resolve().declaration.qualifiedName?.asString() == name } + .map { this to it } + superTypes.forEach { (it.resolve().declaration as KSClassDeclaration).collectAnnotations() } + } + + collectAnnotations() + return pairs +} + /** * Check if the given symbol is valid. */ diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/BaseKspTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/BaseKspTest.kt index 5b882e88..c47dc6eb 100644 --- a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/BaseKspTest.kt +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/BaseKspTest.kt @@ -61,7 +61,7 @@ abstract class BaseKspTest { private fun compile(@Language("kotlin") code: String, options: Map): KotlinCompilation.Result { @Language("kotlin") - val header = """ + val source = """ import java.math.* import java.sql.* import java.time.* @@ -73,12 +73,13 @@ abstract class BaseKspTest { import org.ktorm.entity.* import org.ktorm.ksp.annotation.* + $code + lateinit var database: Database """.trimIndent() - val source = header + code printFile(source, "Source.kt") val compilation = createCompilation(SourceFile.kotlin("Source.kt", source), options) diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/SuperTableClassTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/SuperTableClassTest.kt new file mode 100644 index 00000000..964f26bf --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/SuperTableClassTest.kt @@ -0,0 +1,287 @@ +package org.ktorm.ksp.compiler.generator; + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + + +class SuperTableClassTest : BaseKspTest() { + + @Test + fun defaultSuperTableClass() = runKotlin( + """ + import kotlin.reflect.full.isSubclassOf + + @Table + interface User : Entity { + var id: Int + var name: String + } + + @Table + data class Department( + val id: Int, + val name: String + ) + + fun run() { + assert(Users::class.isSubclassOf(org.ktorm.schema.Table::class)) + assert(Departments::class.isSubclassOf( org.ktorm.schema.BaseTable::class)) + assert(!Departments::class.isSubclassOf(org.ktorm.schema.Table::class)) + } + """.trimIndent() + ) + + @Test + fun superTableClassOnClass() = kspFailing( + "SuperTableClass annotation can only be used on interface.", """ + abstract class UserBaseTable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : org.ktorm.schema.Table( + tableName, alias, catalog, schema, entityClass + ) + + @Table + @SuperTableClass(UserBaseTable::class) + data class User( + val id: Int, + val name: String + ) + """.trimIndent() + ) + + @Test + fun directUseSuperTableClass() = runKotlin( + """ + import kotlin.reflect.full.isSubclassOf + + abstract class UserBaseTable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : org.ktorm.schema.Table( + tableName, alias, catalog, schema, entityClass + ){ + fun foo() = 0 + fun bar() = 1 + } + + @Table + @SuperTableClass(UserBaseTable::class) + interface User : Entity { + var id: Int + var name: String + } + + fun run() { + assert(Users::class.isSubclassOf(UserBaseTable::class)) + } + """.trimIndent() + ) + + @Test + fun singleSuperTableClass() = runKotlin( + """ + import kotlin.reflect.full.isSubclassOf + import org.ktorm.schema.long + + @JvmInline + value class UID(val value: Long) + + @SuperTableClass(UserBaseTable::class) + interface UserBaseEntity> : Entity { + var uid: UID + } + + abstract class UserBaseTable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : org.ktorm.schema.Table( + tableName, alias, catalog, schema, entityClass + ) { + val uid = long("uid").transform({ UID(it) }, { it.value }).primaryKey().bindTo { it.uid } + } + + @Table + interface User : UserBaseEntity { + var name: String + } + + fun run() { + assert(Users::class.isSubclassOf(UserBaseTable::class)) + } + """.trimIndent() + ) + + @Test + fun multipleSuperClassTable() = runKotlin( + """ + import org.ktorm.schema.int + import kotlin.reflect.full.isSubclassOf + + @SuperTableClass(ATable::class) + interface AEntity> : Entity { + var a: Int + } + abstract class ATable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : org.ktorm.schema.Table( + tableName, alias, catalog, schema, entityClass + ) { + val a = int("a").bindTo { it.a } + } + + @SuperTableClass(BTable::class) + interface BEntity> : AEntity { + var b: Int + } + abstract class BTable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : ATable( + tableName, alias, catalog, schema, entityClass + ) { + val b = int("b").bindTo { it.b } + } + + @Table(className = "CTable") + interface CEntity : BEntity { + var c: Int + } + + fun run() { + assert(CTable::class.isSubclassOf(ATable::class)) + assert(CTable::class.isSubclassOf(BTable::class)) + } + """.trimIndent() + ) + + @Test + fun multipleSuperClassButHaveNotSameInheritanceHierarchy() = kspFailing( + "the values of annotation are not in the same inheritance hierarchy.", """ + import org.ktorm.schema.int + import kotlin.reflect.full.isSubclassOf + + @SuperTableClass(ATable::class) + interface AEntity> : Entity { + var a: Int + } + abstract class ATable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : org.ktorm.schema.Table( + tableName, alias, catalog, schema, entityClass + ) { + val a = int("a").bindTo { it.a } + } + + @SuperTableClass(BTable::class) + interface BEntity> : Entity { + var b: Int + } + abstract class BTable>( + tableName: String, + alias: String? = null, + catalog: String? = null, + schema: String? = null, + entityClass: KClass? = null, + ) : org.ktorm.schema.Table( + tableName, alias, catalog, schema, entityClass + ) { + val b = int("b").bindTo { it.b } + } + + @Table(className = "CTable") + interface CEntity : AEntity, BEntity { + var c: Int + } + """.trimIndent() + ) + + @Test + fun minimumSuperTableClassParameter() = runKotlin( + """ + import kotlin.reflect.full.isSubclassOf + + abstract class UserBaseTable>( + tableName: String, + alias: String? = null, + ) : org.ktorm.schema.Table( + tableName, alias + ) + + @Table + @SuperTableClass(UserBaseTable::class) + interface User : Entity { + var id: Int + var name: String + } + + fun run() { + assert(Users::class.isSubclassOf(UserBaseTable::class)) + } + """.trimIndent() + ) + + @Test + fun lackPrimaryConstructor() = kspFailing( + "should have a primary constructor with parameters tableName and alias.", """ + abstract class UserBaseTable> : org.ktorm.schema.Table("t_user") + + @Table + @SuperTableClass(UserBaseTable::class) + interface User : Entity { + var id: Int + var name: String + } + """.trimIndent() + ) + + @Test + fun lackAliasParameter() = kspFailing( + "should have a primary constructor with parameters tableName and alias.", """ + abstract class UserBaseTable>(tableName: String) : org.ktorm.schema.Table(tableName) + + @Table + @SuperTableClass(UserBaseTable::class) + interface User : Entity { + var id: Int + var name: String + } + """.trimIndent() + ) + + @Test + fun lackTableNameParameter() = kspFailing( + "should have a primary constructor with parameters tableName and alias.", """ + abstract class UserBaseTable>(alias: String?) : org.ktorm.schema.Table(alias = alias) + + @Table + @SuperTableClass(UserBaseTable::class) + interface User : Entity { + var id: Int + var name: String + } + """.trimIndent() + ) + +} + diff --git a/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/TableMetadata.kt b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/TableMetadata.kt index 25ed0b15..f43744a5 100644 --- a/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/TableMetadata.kt +++ b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/TableMetadata.kt @@ -17,6 +17,7 @@ package org.ktorm.ksp.spi import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.ClassName /** * Table definition metadata. @@ -66,5 +67,10 @@ public data class TableMetadata( /** * Columns in the table. */ - val columns: List + val columns: List, + + /** + * the super table class of generated table class. + */ + val superClass: ClassName )