diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 61be446..c7076f6 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,7 @@ repositories { } dependencies { - implementation(kotlin("gradle-plugin", "1.8.21")) - implementation("com.huanshankeji:common-gradle-dependencies:0.6.0-20230609") - implementation("com.huanshankeji.team:gradle-plugins:0.4.1") + implementation(kotlin("gradle-plugin", "1.9.23")) + implementation("com.huanshankeji:common-gradle-dependencies:0.7.1-20240314") + implementation("com.huanshankeji.team:gradle-plugins:0.5.1") } diff --git a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt index 7d7ae12..d3c3958 100644 --- a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt +++ b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt @@ -2,5 +2,6 @@ import com.huanshankeji.CommonDependencies import com.huanshankeji.CommonVersions val projectVersion = "0.1.0-SNAPSHOT" -val commonVersions = CommonVersions(kotlin = "1.8.21") + +val commonVersions = CommonVersions() val commonDependencies = CommonDependencies(commonVersions) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8838ba9..2ea3535 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 1fa805d..a4f2481 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -3,4 +3,8 @@ plugins { } dependencies { + implementation(commonDependencies.exposed.core()) + //implementation(commonDependencies.kotlinCommon.exposed()) + implementation(commonDependencies.kotlinCommon.reflect()) + implementation(commonDependencies.kotlinCommon.core()) } diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt new file mode 100644 index 0000000..5610b4d --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt @@ -0,0 +1,37 @@ +package com.huanshankeji.exposed.datamapping + +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.ColumnSet +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.statements.UpdateBuilder + +fun interface SimpleNullableDataQueryMapper { + fun resultRowToData(resultRow: ResultRow): Data +} + +fun interface SimpleDataQueryMapper : SimpleNullableDataQueryMapper + +fun interface NullableDataUpdateMapper { + fun setUpdateBuilder(data: Data, updateBuilder: UpdateBuilder<*>) +} + +fun interface DataUpdateMapper : NullableDataUpdateMapper + +fun DataUpdateMapper.updateBuilderSetter(data: Data): + ColumnSetT.(UpdateBuilder<*>) -> Unit = { + setUpdateBuilder(data, it) +} + +interface SimpleDataMapper : SimpleDataQueryMapper, DataUpdateMapper + + +interface NullableDataQueryMapper : SimpleNullableDataQueryMapper { + val neededColumns: List> +} + +interface DataQueryMapper : NullableDataQueryMapper + +interface NullableDataMapper : NullableDataQueryMapper, NullableDataUpdateMapper + +// TODO rename to `NotNullDataMapper` +interface DataMapper : NullableDataMapper, DataQueryMapper, SimpleDataMapper diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt new file mode 100644 index 0000000..822e9f9 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt @@ -0,0 +1,652 @@ +package com.huanshankeji.exposed.datamapping.classproperty + +import com.huanshankeji.BidirectionalConversion +import com.huanshankeji.exposed.datamapping.DataMapper +import com.huanshankeji.exposed.datamapping.NullableDataMapper +import com.huanshankeji.exposed.datamapping.classproperty.OnDuplicateColumnPropertyNames.CHOOSE_FIRST +import com.huanshankeji.exposed.datamapping.classproperty.OnDuplicateColumnPropertyNames.THROW +import com.huanshankeji.exposed.datamapping.classproperty.PropertyColumnMapping.* +import com.huanshankeji.kotlin.reflect.fullconcretetype.FullConcreteTypeClass +import com.huanshankeji.kotlin.reflect.fullconcretetype.FullConcreteTypeProperty1 +import com.huanshankeji.kotlin.reflect.fullconcretetype.fullConcreteTypeClassOf +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.UpdateBuilder +import org.slf4j.LoggerFactory +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.typeOf +import kotlin.sequences.Sequence + +// Our own class mapping implementation using reflection which should be adapted using annotation processors and code generation in the future. + + +// TODO: unify type parameter names + +typealias PropertyColumnMappings = List> +//typealias LessStrictlyTypedPropertyColumnMappings = List> +/** In the order of the constructor arguments. */ +typealias ClassPropertyColumnMappings = PropertyColumnMappings + +/* +TODO (ADT normalization) consider decoupling/removing `property` and `Data` from this class and renaming it to `ColumnMapping` + and add a `PropertyColumnMapping` containing the property and the `ColumnMapping`. + However, after the refactor, `ColumnMapping` will still be coupled with `ClassPropertyColumnMappings` which is coupled with `PropertyColumnMapping`, + so I am not sure whether this is necessary. +*/ +sealed class PropertyColumnMapping(val fctProperty: FullConcreteTypeProperty1) { + class SqlPrimitive( + fctProperty: FullConcreteTypeProperty1, + val column: Column + ) : PropertyColumnMapping(fctProperty) + + class NestedClass( + fctProperty: FullConcreteTypeProperty1, + val nullability: Nullability, + val adt: Adt + ) : PropertyColumnMapping(fctProperty) { + sealed class Nullability { + class NonNullable : Nullability() + class Nullable(val whetherNullDependentColumn: Column<*>) : + Nullability() + } + + // ADT: algebraic data type + sealed class Adt { + class Product(val nestedMappings: ClassPropertyColumnMappings) : + Adt() + + class Sum( + val subclassMap: Map, Product>, + val sumTypeCaseConfig: SumTypeCaseConfig + ) : Adt() { + val columnsForAllSubclasses = buildSet { + for (subclassProductMapping in subclassMap.values) + addAll(subclassProductMapping.nestedMappings.getColumnSet()) + }.toList() + } + } + } + + class Custom( + fctProperty: FullConcreteTypeProperty1, + val nullableDataMapper: NullableDataMapper + ) : PropertyColumnMapping(fctProperty) + + class Skip(fctProperty: FullConcreteTypeProperty1) : + PropertyColumnMapping(fctProperty) +} + +class SumTypeCaseConfig( + val caseValueColumn: Column, + val caseValueKClassConversion: BidirectionalConversion>, + /* + val caseValueToClass: (CaseValue) -> KClass, + /** + * The [KClass] parameter is null to set a default value for the case value column when the corresponding data is `null`. + */ + val classToCaseValue: (KClass?) -> CaseValue + */ +) + + +// see: https://kotlinlang.org/docs/basic-types.html, https://www.postgresql.org/docs/current/datatype.html +// Types that are commented out are not ensured to work yet. +val defaultNotNullExposedSqlPrimitiveClasses = listOf( + Byte::class, Short::class, Int::class, Long::class, /*BigInteger::class,*/ + UByte::class, UShort::class, UInt::class, ULong::class, + Float::class, Double::class, /*BigDecimal::class,*/ + Boolean::class, + ByteArray::class, + //Char::class, + String::class, + // types related to time and date +) + +private fun KClass<*>.isEnumClass() = + isSubclassOf(Enum::class) + +fun KClass<*>.isExposedSqlPrimitiveType(): Boolean = + this in defaultNotNullExposedSqlPrimitiveClasses || isEnumClass() + +fun KType.isExposedSqlPrimitiveType() = + (classifier as KClass<*>).isExposedSqlPrimitiveType() + +class ColumnWithPropertyName(val propertyName: String, val column: Column<*>) + +fun getColumnsWithPropertyNamesWithoutTypeParameter( + table: Table, clazz: KClass = table::class +): Sequence = + getColumnProperties(clazz).map { + @Suppress("UNCHECKED_CAST") + ColumnWithPropertyName(it.name, (it as KProperty1>)(table)) + } + +enum class OnDuplicateColumnPropertyNames { + CHOOSE_FIRST, THROW +} + +fun getColumnByPropertyNameMap( + tables: List, + onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST // defaults to `CHOOSE_FIRST` because there are very likely to be duplicate columns when joining table +): Map> { + val columnsMap = tables.asSequence() + .flatMap { table -> getColumnsWithPropertyNamesWithoutTypeParameter(table) } + .groupBy { it.propertyName } + return columnsMap.mapValues { + it.value.run { + when (onDuplicateColumnPropertyNames) { + CHOOSE_FIRST -> first() + THROW -> single() + } + .column + } + } +} + +private val logger = LoggerFactory.getLogger("class property mapping") // TODO: rename + +// Open or abstract but not sealed classes are not supported after this refactor because non-sealed subclasses are not supported yet in `FullConcreteTypeClass`. + +// TODO remove or support open and abstract classes again +private fun KClass<*>.isInheritable() = + /*isOpen || isAbstract ||*/ isSealed + +// TODO remove or support open and abstract classes again +private fun KClass<*>.isAbstractOrSealed() = + /*isAbstract ||*/ isSealed + +/** + * @param skip both writing and reading. Note that the property type need not be nullable if it's only used for writing. + * @param whetherNullDependentColumn required for nullable properties. + * @param adt sub-configs for properties and subclasses. + */ +// TODO refactor the mutually exclusive arguments to sum type constructors (sealed subclasses) +class PropertyColumnMappingConfig

( + type: KType, + val skip: Boolean = false, + // TODO Support query-only mappers, and update-only mappers. However this may require HKT or unsafe casting. + val customMapper: NullableDataMapper

? = null, + usedForQuery: Boolean = true, + val columnPropertyName: String? = null, // TODO: use the property directly instead of the name string + val whetherNullDependentColumn: Column<*>? = null, // for query + /* TODO: whether it's null can depend on all columns: + the property is null if when all columns are null (warn if some columns are not null), + or a necessary column is null, + in both cases of which warn if all nested properties are nullable */ + val adt: Adt

? = null, // for query and update +) { + init { + // perform the checks + + // TODO log property information instead of just property return type information in the messages + if (type.isMarkedNullable) { + if (skip && whetherNullDependentColumn !== null || adt !== null) + logger.warn("${::whetherNullDependentColumn.name} and ${::adt.name} are unnecessary when ${::skip.name} is configured to true for $type.") + } else { + // Non-nullable properties can be skipped when updating but not when querying. + if (usedForQuery) + require(!skip) + require(whetherNullDependentColumn === null) + } + + + if (type.isExposedSqlPrimitiveType()) { + if (whetherNullDependentColumn !== null) + logger.warn("${::whetherNullDependentColumn} is set for a primitive type $type and will be ignored.") + if (adt !== null) + logger.warn("${::adt} is set for a primitive type $type and will be ignored.") + } + @Suppress("UNCHECKED_CAST") + val clazz = type.classifier as KClass

+ when (adt) { + is Adt.Product -> require(clazz.isFinal || clazz.isOpen) { "the class $clazz must be instantiable (final or open) to be treated as a product type" } + is Adt.Sum<*, *> -> require(clazz.isInheritable()) { "the class $clazz must be inheritable (open, abstract, or sealed) to be treated as a sum type" } + null -> {} + } + } + + companion object { + inline fun create( + skip: Boolean = false, + customMapper: NullableDataMapper? = null, + usedForQuery: Boolean = true, + columnPropertyName: String? = null, // TODO: use the column property + nullDependentColumn: Column<*>? = null, + adt: Adt? = null + ) = + PropertyColumnMappingConfig( + typeOf(), skip, customMapper, usedForQuery, columnPropertyName, nullDependentColumn, adt + ) + } + + // ADT: algebraic data type + sealed class Adt { + class Product(val nestedConfigMap: PropertyColumnMappingConfigMap) : + Adt() + + class Sum( + clazz: KClass, + val subclassProductConfigMapOverride: Map, Product>, // TODO: why can't a sum type nest another sum type? + val sumTypeCaseConfig: SumTypeCaseConfig + ) : Adt() { + init { + require(subclassProductConfigMapOverride.keys.all { !it.isInheritable() && it.isSubclassOf(clazz) }) + } + + companion object { + inline fun createForSealed( + subclassProductConfigMapOverride: Map, Product> = emptyMap(), + sumTypeCaseConfig: SumTypeCaseConfig + ): Sum { + val clazz = Data::class + require(clazz.isSealed) + return Sum(clazz, subclassProductConfigMapOverride, sumTypeCaseConfig) + } + + // TODO remove or support open and abstract classes again + inline fun createForAbstract( + subclassProductConfigMap: Map, Product>, + sumTypeCaseConfig: SumTypeCaseConfig + ): Sum { + val clazz = Data::class + require(clazz.isAbstract) + return Sum(clazz, subclassProductConfigMap, sumTypeCaseConfig) + } + } + } + + // not needed + //class Enum, CaseValue> : Adt() + } +} + +typealias PropertyColumnMappingConfigMap2 = Map, PropertyColumnMappingConfig> +// TODO Constrain the property return type and the config type parameter to be the same. Consider using builder DSLs. +typealias PropertyColumnMappingConfigMap = PropertyColumnMappingConfigMap2 + +private fun KClass<*>.isObject() = + objectInstance !== null + +private fun doGetDefaultClassPropertyColumnMappings( + fullConcreteTypeClass: FullConcreteTypeClass, + tables: List

, // for error messages only + columnByPropertyNameMap: Map>, // TODO: refactor as `Data` may be a sum type + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() + /* TODO Constructing `FullConcreteTypeProperty1` seems complicated after the code is refactored. + Consider refactoring `PropertyColumnMapping` with one extra `Property` type parameter and apply simple `KProperty` for `customMappings`, + or merging it into config. */ +): ClassPropertyColumnMappings { + val customMappingPropertySet = customMappings.asSequence().map { it.fctProperty }.toSet() + + val dataFctMemberPropertyMap = fullConcreteTypeClass.memberProperties.asSequence() + .filterNot { it in customMappingPropertySet } + .associateBy { it.kProperty.name } + val customMappingMap = customMappings.associateBy { it.fctProperty.kProperty.name } + + val kClass = fullConcreteTypeClass.kClass + return if (kClass.isObject()) // mainly for case objects of sealed classes + emptyList() // TODO: use `null` + else (kClass.primaryConstructor + ?: throw IllegalArgumentException("$kClass must have a primary constructor with all the properties to be mapped to columns to be mapped as a product type")) + .parameters.map { + val name = it.name!! + + val customMapping = customMappingMap[name] + if (customMapping !== null) + return@map customMapping + + val fctProperty = dataFctMemberPropertyMap.getOrElse(name) { + throw IllegalArgumentException("primary constructor parameter `$it` is not a property in the class `$kClass`") + } + val kProperty = fctProperty.kProperty + require(it.type == kProperty.returnType) { + "primary constructor parameter `$it` and property `$kProperty` have different types" + } + + // This function is added to introduce a new type parameter `PropertyData` to constrain the types better. + fun typeParameterHelper(fctProperty: FullConcreteTypeProperty1): PropertyColumnMapping { + @Suppress("NAME_SHADOWING") + val kProperty = fctProperty.kProperty + val config = + propertyColumnMappingConfigMapOverride[kProperty] as PropertyColumnMappingConfig? + if (config?.skip == true) + return Skip(fctProperty) + config?.customMapper?.let { + return Custom(fctProperty, it) + } + + val columnPropertyName = config?.columnPropertyName ?: name + val propertyReturnTypeFctClass = fctProperty.returnType + val propertyReturnTypeKClass = propertyReturnTypeFctClass.kClass + val propertyReturnTypeKType = propertyReturnTypeFctClass.kType + + return if (propertyReturnTypeKClass.isExposedSqlPrimitiveType()) + @Suppress("UNCHECKED_CAST") + SqlPrimitive( + fctProperty, columnByPropertyNameMap.getOrElse(columnPropertyName) { + throw IllegalArgumentException("column with property name `$columnPropertyName` for class property `$kProperty` does not exist in the tables `$tables`") + } as Column + ) + else { + val isNullable = propertyReturnTypeKType.isMarkedNullable + + @Suppress("UNCHECKED_CAST") + val nullability = + ( + if (isNullable) + /* + I first had the idea of finding a default `nullDependentColumn` but it seems difficult to cover all kinds of cases. + + There are 3 ways I can think of to find the default `nullDependentColumn` in the corresponding columns mapped by the properties: + 1. find the first non-nullable column; + 1. find the first column that's a primary key; + 1. find the first non-nullable column with the suffix "id". + + They all have their drawbacks. + The first approach is too unpredictable, adding or removing properties can affect which column to choose. + Both the second and the third approach can't deal with the case where the column is not within the mapped columns, + which happens when selecting a small portion of the fields as data. + */ + NestedClass.Nullability.Nullable( + config?.whetherNullDependentColumn + ?: throw IllegalArgumentException("`PropertyColumnMappingConfig::nullDependentColumn` has to be specified for `$kProperty` because its return type `$propertyReturnTypeFctClass` is a nullable nested data type") + ) + else + NestedClass.Nullability.NonNullable() + ) + as NestedClass.Nullability + + + @Suppress("UNCHECKED_CAST") + val adtConfig = config?.adt as PropertyColumnMappingConfig.Adt? + val adt = if (propertyReturnTypeKClass.isAbstractOrSealed()) { + //requireNotNull(adtConfig) + require(adtConfig is PropertyColumnMappingConfig.Adt.Sum<*, *>) + adtConfig as PropertyColumnMappingConfig.Adt.Sum + val subclassProductConfigMapOverride = adtConfig.subclassProductConfigMapOverride + + val sealedLeafFctSubclasses = + propertyReturnTypeFctClass.sealedLeafSubclasses() // TODO: also support direct sealed subtypes + val subclassProductNestedConfigMapMapOverride = + subclassProductConfigMapOverride.mapValues { it.value.nestedConfigMap } + val subclassProductNestedConfigMapMap = + if (propertyReturnTypeKClass.isSealed) + sealedLeafFctSubclasses.asSequence().map { it.kClass }.associateWith { + emptyMap, PropertyColumnMappingConfig<*>>() + } + subclassProductNestedConfigMapMapOverride + else { + require(subclassProductConfigMapOverride.isNotEmpty()) { "A custom config needs to be specified for a non-sealed abstract class $propertyReturnTypeKType" } + subclassProductNestedConfigMapMapOverride + } + + NestedClass.Adt.Sum( + sealedLeafFctSubclasses.associate { + fun typeParameterHelper( + fullConcreteTypeClass: FullConcreteTypeClass, + configMap: PropertyColumnMappingConfigMap + ): NestedClass.Adt.Product = + NestedClass.Adt.Product( + doGetDefaultClassPropertyColumnMappings( + fullConcreteTypeClass, + tables, columnByPropertyNameMap, + configMap + ) + ) + + @Suppress("NAME_SHADOWING") + val kClass = it.kClass + @Suppress("UNCHECKED_CAST") + kClass to typeParameterHelper( + it as FullConcreteTypeClass, + subclassProductNestedConfigMapMap[kClass] as PropertyColumnMappingConfigMap + ) + }, + adtConfig.sumTypeCaseConfig + ) + } else { + require(adtConfig is PropertyColumnMappingConfig.Adt.Product?) + NestedClass.Adt.Product( + doGetDefaultClassPropertyColumnMappings( + propertyReturnTypeFctClass, + tables, columnByPropertyNameMap, + (adtConfig?.nestedConfigMap ?: emptyMap()) + ) + ) + } + + NestedClass(fctProperty, nullability, adt) + } + } + typeParameterHelper(fctProperty) + } +} + +fun getDefaultClassPropertyColumnMappings( + fullConcreteTypeClass: FullConcreteTypeClass, + tables: List
, onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() +): ClassPropertyColumnMappings = + doGetDefaultClassPropertyColumnMappings( + fullConcreteTypeClass, + tables, getColumnByPropertyNameMap(tables, onDuplicateColumnPropertyNames), + propertyColumnMappingConfigMapOverride, + customMappings + ) + +// TODO: decouple query mapper and update mapper. +/** Supports classes with nested composite class properties and multiple tables */ +class ReflectionBasedClassPropertyDataMapper( + val fullConcreteTypeClass: FullConcreteTypeClass, + val classPropertyColumnMappings: ClassPropertyColumnMappings, +) : DataMapper { + override val neededColumns = classPropertyColumnMappings.getColumnSet().toList() + override fun resultRowToData(resultRow: ResultRow): Data = + constructDataWithResultRow(fullConcreteTypeClass, classPropertyColumnMappings, resultRow) + + override fun setUpdateBuilder(data: Data, updateBuilder: UpdateBuilder<*>) { + setUpdateBuilder(classPropertyColumnMappings, data, updateBuilder) + } +} + + +private fun constructDataWithResultRow( + fctClass: FullConcreteTypeClass, + classPropertyColumnMappings: ClassPropertyColumnMappings, + resultRow: ResultRow +): Data = + fctClass.kClass.primaryConstructor!!.call(*classPropertyColumnMappings.map { + fun typeParameterHelper( + propertyColumnMapping: PropertyColumnMapping, + nestedFctClass: FullConcreteTypeClass + ) = + when (propertyColumnMapping) { + is SqlPrimitive -> resultRow.getValue(propertyColumnMapping.column) + is NestedClass -> { + fun constructNotNullData() = + when (val adt = propertyColumnMapping.adt) { + is NestedClass.Adt.Product -> + constructDataWithResultRow(nestedFctClass, adt.nestedMappings, resultRow) + + is NestedClass.Adt.Sum -> { + fun typeParameterHelper(sum: NestedClass.Adt.Sum): SubclassData { + val subclass = with(sum.sumTypeCaseConfig) { + caseValueKClassConversion.to(resultRow[caseValueColumn]) + } + @Suppress("UNCHECKED_CAST") + return constructDataWithResultRow( + subclass as FullConcreteTypeClass, + sum.subclassMap.getValue(subclass).nestedMappings as ClassPropertyColumnMappings, + resultRow + ) + } + typeParameterHelper(adt) + } + } + + when (val nullability = propertyColumnMapping.nullability) { + is NestedClass.Nullability.NonNullable -> constructNotNullData() + is NestedClass.Nullability.Nullable<*> -> if (resultRow[nullability.whetherNullDependentColumn] !== null) constructNotNullData() else null + } + } + + is Custom -> propertyColumnMapping.nullableDataMapper.resultRowToData(resultRow) + is Skip -> null + } + @Suppress("UNCHECKED_CAST") + typeParameterHelper( + it as PropertyColumnMapping, + it.fctProperty.returnType + ) + }.toTypedArray()) + +fun setUpdateBuilder( + classPropertyColumnMappings: ClassPropertyColumnMappings, data: Data, updateBuilder: UpdateBuilder<*> +) { + for (propertyColumnMapping in classPropertyColumnMappings) { + fun typeParameterHelper(propertyColumnMapping: PropertyColumnMapping) { + val propertyData = propertyColumnMapping.fctProperty.kProperty(data) + when (propertyColumnMapping) { + is SqlPrimitive -> + updateBuilder[propertyColumnMapping.column] = propertyData + + is NestedClass -> { + // `propertyColumnMapping.nullability` is not needed here + when (val adt = propertyColumnMapping.adt) { + is NestedClass.Adt.Product -> { + val nestedMappings = adt.nestedMappings + if (propertyData !== null) + setUpdateBuilder(nestedMappings, propertyData, updateBuilder) + else + setUpdateBuilderColumnsToNullsWithMappings(nestedMappings, updateBuilder) + } + + is NestedClass.Adt.Sum -> { + fun typeParameterHelper() { + @Suppress("UNCHECKED_CAST") + adt as NestedClass.Adt.Sum + with(adt) { + if (propertyData !== null) { + // TODO: it seems to be a compiler bug that the non-null assertion is needed here. see: https://youtrack.jetbrains.com/issue/KT-37878/No-Smart-cast-for-class-literal-reference-of-nullable-generic-type. + val propertyDataClass = propertyData!!::class + with(sumTypeCaseConfig) { + updateBuilder[caseValueColumn] = + caseValueKClassConversion.from(propertyDataClass) + } + fun typeParameterHelper( + subclassMapping: NestedClass.Adt.Product, + propertyData: SubclassData + ) = + setUpdateBuilder( + subclassMapping.nestedMappings, propertyData, updateBuilder + ) + @Suppress("UNCHECKED_CAST") + typeParameterHelper( + subclassMap.getValue(propertyDataClass) as NestedClass.Adt.Product, + propertyData + ) + } else { + with(sumTypeCaseConfig) { + @Suppress("UNCHECKED_CAST") + updateBuilder[caseValueColumn as Column] = null + } + setUpdateBuilderColumnsToNulls(columnsForAllSubclasses, updateBuilder) + } + } + } + typeParameterHelper() + } + } + } + + is Custom -> + propertyColumnMapping.nullableDataMapper.setUpdateBuilder(propertyData, updateBuilder) + + is Skip -> {} + } + } + + typeParameterHelper(propertyColumnMapping) + } +} + +fun PropertyColumnMapping<*, *>.forEachColumn(block: (Column<*>) -> Unit) = + when (this) { + is SqlPrimitive -> block(column) + is NestedClass -> { + when (nullability) { + is NestedClass.Nullability.NonNullable -> {} + is NestedClass.Nullability.Nullable -> block(nullability.whetherNullDependentColumn) + } + when (adt) { + is NestedClass.Adt.Product -> adt.nestedMappings.forEachColumn(block) + is NestedClass.Adt.Sum<*, *> -> { + block(adt.sumTypeCaseConfig.caseValueColumn) + adt.subclassMap.values.forEach { it.nestedMappings.forEachColumn(block) } + } + } + } + + is Custom -> nullableDataMapper.neededColumns.forEach(block) + is Skip -> {} + } + +fun ClassPropertyColumnMappings<*>.forEachColumn(block: (Column<*>) -> Unit) { + for (propertyColumnMapping in this) + propertyColumnMapping.forEachColumn(block) +} + +fun setUpdateBuilderColumnsToNullsWithMappings( + classPropertyColumnMappings: ClassPropertyColumnMappings<*>, updateBuilder: UpdateBuilder<*> +) = + classPropertyColumnMappings.forEachColumn { + @Suppress("UNCHECKED_CAST") + updateBuilder[it as Column] = null + } + +fun setUpdateBuilderColumnsToNulls(columns: List>, updateBuilder: UpdateBuilder<*>) { + for (column in columns) + @Suppress("UNCHECKED_CAST") + updateBuilder[column as Column] = null +} + +fun ClassPropertyColumnMappings<*>.getColumnSet(): Set> = + buildSet { forEachColumn { add(it) } } + +// TODO add a version of `reflectionBasedClassPropertyDataMapper` that takes column properties and make the following 2 functions depend on it + +inline fun reflectionBasedClassPropertyDataMapper( + tables: List
, + onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() // TODO consider removing this parameter if possible +): ReflectionBasedClassPropertyDataMapper { + val fullConcreteTypeClass = fullConcreteTypeClassOf() + return ReflectionBasedClassPropertyDataMapper( + fullConcreteTypeClass, getDefaultClassPropertyColumnMappings( + fullConcreteTypeClass, + tables, onDuplicateColumnPropertyNames, propertyColumnMappingConfigMapOverride, customMappings + ) + ) +} + +inline fun reflectionBasedClassPropertyDataMapper( + table: Table, + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() +) = + reflectionBasedClassPropertyDataMapper(listOf(table), THROW, propertyColumnMappingConfigMapOverride, customMappings) + +/** + * A shortcut for [Join]s. + */ +inline fun reflectionBasedClassPropertyDataMapper( + join : Join, + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() +) = + reflectionBasedClassPropertyDataMapper(join.targetTables(), CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings) diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt new file mode 100644 index 0000000..8dcf713 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt @@ -0,0 +1,104 @@ +package com.huanshankeji.exposed.datamapping.classproperty + +import com.huanshankeji.exposed.datamapping.SimpleDataMapper +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.UpdateBuilder +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.sequences.Sequence + +fun ResultRow.getValue(column: Column<*>): Any? = + this[column].let { + if (it is EntityID<*>) it.value else it + } + +/** Nested classes are not supported. */ +interface ReflectionBasedSimpleClassPropertyDataMapper : SimpleDataMapper { + val propertyAndColumnPairs: List, Column<*>>> + val dataPrimaryConstructor: KFunction + + override fun resultRowToData(resultRow: ResultRow): Data { + val params = propertyAndColumnPairs.map { (_, column) -> resultRow.getValue(column) } + return dataPrimaryConstructor.call(*params.toTypedArray()) + } + + override fun setUpdateBuilder(data: Data, updateBuilder: UpdateBuilder<*>) { + for ((property, column) in propertyAndColumnPairs) + @Suppress("UNCHECKED_CAST") + updateBuilder[column as Column] = property(data) + } +} + +// The `TableT` reified type parameter is passed to the functions called and should not be removed. +inline fun reflectionBasedSimpleClassPropertyDataMapper(table: TableT): ReflectionBasedSimpleClassPropertyDataMapper = + object : ReflectionBasedSimpleClassPropertyDataMapper { + private val clazz = Data::class + + // This property needs to initialize first. + override val dataPrimaryConstructor = clazz.primaryConstructor!! + override val propertyAndColumnPairs = run { + //require(dClass.isData) + val dataMemberPropertyMap = clazz.memberProperties.associateBy { it.name } + val columnMap = getColumnByPropertyNameMapWithTypeParameter(table) + dataPrimaryConstructor.parameters.map { + val name = it.name!! + dataMemberPropertyMap.getValue(name) to columnMap.getValue(name) + } + } + } + +@Suppress("UNCHECKED_CAST") +fun getColumnProperties(clazz: KClass): Sequence>> = + clazz.memberProperties.asSequence() + .filter { it.returnType.run { classifier == Column::class && !isMarkedNullable } } + as Sequence>> + +fun getColumnPropertyByNameMap(clazz: KClass): Map>> = + getColumnProperties(clazz).associateBy { it.name } + +inline fun getColumnByPropertyNameMapWithTypeParameter(table: TableT): Map> = + getColumnPropertyByNameMap(TableT::class) + .mapValues { it.value(table) } + +inline fun reflectionBasedSimpleClassPropertyDataMapperForAlias( + tableDataMapper: ReflectionBasedSimpleClassPropertyDataMapper, alias: Alias +): ReflectionBasedSimpleClassPropertyDataMapper = + object : ReflectionBasedSimpleClassPropertyDataMapper { + override val dataPrimaryConstructor = tableDataMapper.dataPrimaryConstructor + override val propertyAndColumnPairs = + tableDataMapper.propertyAndColumnPairs.map { it.first to alias[it.second] } + } + + +inline fun innerJoinResultRowToData( + crossinline resultRowToData1: (ResultRow) -> D1, crossinline resultRowToData2: (ResultRow) -> D2 +): (ResultRow) -> Pair = { + resultRowToData1(it) to resultRowToData2(it) +} + +inline fun leftJoinResultRowToData( + crossinline resultRowToData1: (ResultRow) -> D1, crossinline resultRowToData2: (ResultRow) -> D2, + onColumn: Column<*> +): (ResultRow) -> Pair = { + // `it.hasValue` returns true here but the value is `null` + resultRowToData1(it) to if (it[onColumn] !== null) resultRowToData2(it) else null +} + +inline fun leftJoinResultRowToData( + crossinline resultRowToData1: (ResultRow) -> D1, + crossinline resultRowToData2: (ResultRow) -> D2, + crossinline resultRowToData3: (ResultRow) -> D3, + onColumn2: Column<*>, + onColumn3: Column<*> +): (ResultRow) -> Triple = { + // `it.hasValue` returns `true` here but the value is `null` + Triple( + resultRowToData1(it), + if (it[onColumn2] !== null) resultRowToData2(it) else null, + if (it[onColumn3] !== null) resultRowToData3(it) else null + ) +}