diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 637261b0d..24d9d052e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: true matrix: - java: [8, 11, 17, 19] + java: [8, 11, 17, 20] steps: - name: Checkout Code uses: actions/checkout@v3 diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e624db5df..9c56d76a6 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("org.gradle.kotlin.kotlin-dsl") version "2.4.1" + `kotlin-dsl` } repositories { @@ -9,7 +9,7 @@ repositories { } dependencies { - api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22") + api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") api("org.moditect:moditect-gradle-plugin:1.0.0-rc3") - api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.20.0") + api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1") } diff --git a/buildSrc/src/main/kotlin/ktorm.base.gradle.kts b/buildSrc/src/main/kotlin/ktorm.base.gradle.kts new file mode 100644 index 000000000..453a58840 --- /dev/null +++ b/buildSrc/src/main/kotlin/ktorm.base.gradle.kts @@ -0,0 +1,54 @@ + +group = rootProject.group +version = rootProject.version + +plugins { + id("kotlin") + id("org.gradle.jacoco") + id("io.gitlab.arturbosch.detekt") +} + +repositories { + mavenCentral() +} + +dependencies { + api(kotlin("stdlib")) + api(kotlin("reflect")) + testImplementation(kotlin("test-junit")) + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:${detekt.toolVersion}") +} + +detekt { + source.setFrom("src/main/kotlin") + config.setFrom("${project.rootDir}/detekt.yml") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + allWarningsAsErrors = true + freeCompilerArgs = listOf("-Xexplicit-api=strict") + } + } + + compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } + } + + jacocoTestReport { + reports { + csv.required.set(true) + xml.required.set(true) + html.required.set(true) + } + } +} diff --git a/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts b/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts new file mode 100644 index 000000000..1d6fd4792 --- /dev/null +++ b/buildSrc/src/main/kotlin/ktorm.modularity.gradle.kts @@ -0,0 +1,30 @@ + +plugins { + id("kotlin") + id("org.moditect.gradleplugin") +} + +moditect { + // Generate a multi-release jar, the module descriptor will be located at META-INF/versions/9/module-info.class + addMainModuleInfo { + jvmVersion.set("9") + overwriteExistingFiles.set(true) + module { + moduleInfoFile = file("src/main/moditect/module-info.java") + } + } + + // Let kotlin compiler know the module descriptor. + if (JavaVersion.current() >= JavaVersion.VERSION_1_9) { + sourceSets.main { + kotlin.srcDir("src/main/moditect") + } + } + + // Workaround to avoid circular task dependencies, see https://github.com/moditect/moditect-gradle-plugin/issues/14 + afterEvaluate { + val compileJava = tasks.compileJava.get() + val addDependenciesModuleInfo = tasks.addDependenciesModuleInfo.get() + compileJava.setDependsOn(compileJava.dependsOn - addDependenciesModuleInfo) + } +} diff --git a/buildSrc/src/main/kotlin/ktorm.module.gradle.kts b/buildSrc/src/main/kotlin/ktorm.module.gradle.kts deleted file mode 100644 index 6f401b380..000000000 --- a/buildSrc/src/main/kotlin/ktorm.module.gradle.kts +++ /dev/null @@ -1,78 +0,0 @@ - -group = rootProject.group -version = rootProject.version - -plugins { - id("kotlin") - id("org.gradle.jacoco") - id("org.moditect.gradleplugin") - id("io.gitlab.arturbosch.detekt") - id("ktorm.source-header-check") - id("ktorm.publish") -} - -repositories { - mavenCentral() -} - -dependencies { - api(kotlin("stdlib")) - api(kotlin("reflect")) - testImplementation(kotlin("test-junit")) - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:${detekt.toolVersion}") -} - -moditect { - // Generate a multi-release jar, the module descriptor will be located at META-INF/versions/9/module-info.class - addMainModuleInfo { - jvmVersion.set("9") - overwriteExistingFiles.set(true) - module { - moduleInfoFile = file("src/main/moditect/module-info.java") - } - } - - // Let kotlin compiler know the module descriptor. - if (JavaVersion.current() >= JavaVersion.VERSION_1_9) { - sourceSets.main { - kotlin.srcDir("src/main/moditect") - } - } - - // Workaround to avoid circular task dependencies, see https://github.com/moditect/moditect-gradle-plugin/issues/14 - afterEvaluate { - val compileJava = tasks.compileJava.get() - val addDependenciesModuleInfo = tasks.addDependenciesModuleInfo.get() - compileJava.setDependsOn(compileJava.dependsOn - addDependenciesModuleInfo) - } -} - -detekt { - source = files("src/main/kotlin") - config = files("${project.rootDir}/detekt.yml") -} - -tasks { - compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - allWarningsAsErrors = true - freeCompilerArgs = listOf("-Xexplicit-api=strict") - } - } - - compileTestKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = listOf("-Xjvm-default=all") - } - } - - jacocoTestReport { - reports { - csv.required.set(true) - xml.required.set(true) - html.required.set(true) - } - } -} diff --git a/buildSrc/src/main/kotlin/ktorm.source-header-check.gradle.kts b/buildSrc/src/main/kotlin/ktorm.source-header-check.gradle.kts index 392ca572c..8590632f5 100644 --- a/buildSrc/src/main/kotlin/ktorm.source-header-check.gradle.kts +++ b/buildSrc/src/main/kotlin/ktorm.source-header-check.gradle.kts @@ -5,7 +5,7 @@ plugins { val licenseHeaderText = """ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/buildSrc/src/main/kotlin/ktorm.tuples-codegen.gradle.kts b/buildSrc/src/main/kotlin/ktorm.tuples-codegen.gradle.kts index 57c0a7ffc..0ab69909b 100644 --- a/buildSrc/src/main/kotlin/ktorm.tuples-codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/ktorm.tuples-codegen.gradle.kts @@ -3,7 +3,7 @@ plugins { id("kotlin") } -val generatedSourceDir = "${project.buildDir.absolutePath}/generated/source/main/kotlin" +val generatedSourceDir = "${project.layout.buildDirectory.asFile.get()}/generated/source/main/kotlin" val maxTupleNumber = 9 fun generateTuple(writer: java.io.Writer, tupleNumber: Int) { @@ -282,7 +282,7 @@ val generateTuples by tasks.registering { outputFile.bufferedWriter().use { writer -> writer.write(""" /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/detekt.yml b/detekt.yml index 7b401e2d0..6e1c8b3b4 100644 --- a/detekt.yml +++ b/detekt.yml @@ -52,14 +52,14 @@ complexity: active: true threshold: 12 includeStaticDeclarations: false - ComplexMethod: + CyclomaticComplexMethod: active: true threshold: 20 ignoreSingleWhenExpression: true ignoreSimpleWhenEntries: true LabeledExpression: active: true - ignoredLabels: "" + ignoredLabels: [] LargeClass: active: true threshold: 600 @@ -72,7 +72,7 @@ complexity: constructorThreshold: 6 ignoreDefaultParameters: true MethodOverloading: - active: true + active: false threshold: 7 NestedBlockDepth: active: true @@ -131,7 +131,7 @@ exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true - methodNames: 'toString,hashCode,equals,finalize' + methodNames: ['toString', 'hashCode', 'equals', 'finalize'] InstanceOfCheckForException: active: true NotImplementedDeclaration: @@ -144,14 +144,14 @@ exceptions: active: true SwallowedException: active: true - ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' + ignoredExceptionTypes: ['InterruptedException', 'NumberFormatException', 'ParseException', 'MalformedURLException'] ThrowingExceptionFromFinally: active: true ThrowingExceptionInMain: active: true ThrowingExceptionsWithoutMessageOrCause: active: true - exceptions: 'IllegalArgumentException,IllegalStateException,IOException' + exceptions: ['IllegalArgumentException', 'IllegalStateException', 'IOException'] ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: @@ -195,7 +195,6 @@ formatting: active: true autoCorrect: false indentSize: 4 - continuationIndentSize: 4 MaximumLineLength: active: false maxLineLength: 120 @@ -284,7 +283,7 @@ naming: enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false - forbiddenName: '' + forbiddenName: [] FunctionMaxLength: active: true maximumFunctionNameLength: 64 @@ -295,12 +294,10 @@ naming: active: true functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' excludeClassPattern: '$^' - ignoreOverridden: true FunctionParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true MatchingDeclarationName: active: true MemberNameEqualsClassName: @@ -330,7 +327,6 @@ naming: variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true performance: active: true @@ -345,8 +341,6 @@ performance: potential-bugs: active: true - DuplicateCaseInWhenExpression: - active: true EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: @@ -382,7 +376,7 @@ style: active: true DataClassContainsFunctions: active: false - conversionFunctionPrefix: 'as' + conversionFunctionPrefix: ['as'] EqualsNullCall: active: true EqualsOnSignatureLine: @@ -394,22 +388,22 @@ style: includeLineWrapping: false ForbiddenComment: active: true - values: 'TODO:,FIXME:,STOPSHIP:' + comments: ['FIXME:', 'STOPSHIP:', 'TODO:'] ForbiddenImport: active: false - imports: '' + imports: [] ForbiddenVoid: active: true FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true - excludedFunctions: 'describeContents' + excludedFunctions: ['describeContents'] LoopWithTooManyJumpStatements: active: true maxJumpCount: 2 MagicNumber: active: true - ignoreNumbers: '-1,0,1,2,3,60' + ignoreNumbers: ['-1', '0', '1', '2', '3', '60'] ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreConstantDeclaration: true @@ -417,8 +411,10 @@ style: ignoreAnnotation: false ignoreNamedArgument: true ignoreEnums: false - MandatoryBracesIfStatements: + BracesOnIfStatements: active: true + singleLine: 'never' + multiLine: 'always' MaxLineLength: active: true maxLineLength: 120 @@ -439,8 +435,10 @@ style: active: true OptionalUnit: active: true - OptionalWhenBraces: + BracesOnWhenStatements: active: false + singleLine: 'never' + multiLine: 'necessary' PreferToOverPairSyntax: active: false ProtectedMemberInFinalClass: @@ -450,7 +448,7 @@ style: ReturnCount: active: false max: 2 - excludedFunctions: "equals" + excludedFunctions: ["equals"] excludeLabeled: false excludeReturnFromLambda: true SafeCast: @@ -496,4 +494,4 @@ style: active: true WildcardImport: active: false - excludeImports: 'java.util.*,kotlinx.android.synthetic.*' + excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*'] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f42e62f37..27313fbc8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ktorm-core/ktorm-core.gradle.kts b/ktorm-core/ktorm-core.gradle.kts index db70c7ea9..f405baddb 100644 --- a/ktorm-core/ktorm-core.gradle.kts +++ b/ktorm-core/ktorm-core.gradle.kts @@ -1,6 +1,9 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") id("ktorm.tuples-codegen") } @@ -8,7 +11,7 @@ dependencies { compileOnly("org.springframework:spring-jdbc:5.0.10.RELEASE") compileOnly("org.springframework:spring-tx:5.0.10.RELEASE") testImplementation("com.h2database:h2:1.4.198") - testImplementation("org.slf4j:slf4j-simple:1.7.25") + testImplementation("org.slf4j:slf4j-simple:2.0.3") } val testOutput by configurations.creating { diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt index 0d9924e89..257813fcf 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -20,6 +20,7 @@ import java.io.InputStream import java.io.Reader import java.math.BigDecimal import java.math.BigInteger +import java.net.URI import java.net.URL import java.sql.* import java.sql.Date @@ -45,7 +46,7 @@ import javax.sql.rowset.serial.* * * @since 2.7 */ -@Suppress("LargeClass", "MethodOverloading") +@Suppress("LargeClass") public open class CachedRowSet(rs: ResultSet) : ResultSet { private val _typeMap = readTypeMap(rs) private val _metadata = readMetadata(rs) @@ -1153,7 +1154,7 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { return when (val value = getColumnValue(columnIndex)) { null -> null is URL -> value - is String -> URL(value) + is String -> URI(value).toURL() else -> throw SQLException("Cannot convert ${value.javaClass.name} value to URL.") } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSetMetadata.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSetMetadata.kt index cf3e359d9..ce14f5ec3 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSetMetadata.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSetMetadata.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt index 070216a11..b903d0d04 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcExtensions.kt index b59ad85f7..5a986ea9b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcTransactionManager.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcTransactionManager.kt index 9596689d0..f88cf4346 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcTransactionManager.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/JdbcTransactionManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/Keywords.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/Keywords.kt index e59bfe56b..48935076b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/Keywords.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/Keywords.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/SpringManagedTransactionManager.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/SpringManagedTransactionManager.kt index 37c518b41..1df52ba2f 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/SpringManagedTransactionManager.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/SpringManagedTransactionManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt index c131ad6d0..a8e13f167 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/TransactionManager.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/TransactionManager.kt index 6c872891a..f7a9eae8d 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/TransactionManager.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/TransactionManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -103,7 +103,7 @@ public enum class TransactionIsolation(public val level: Int) { * Find an enum value by the specific isolation level. */ public fun valueOf(level: Int): TransactionIsolation { - return values().first { it.level == level } + return entries.first { it.level == level } } } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Aggregation.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Aggregation.kt index 4479a625f..5bf1d6a96 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Aggregation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Aggregation.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/CaseWhen.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/CaseWhen.kt index cde1868bc..26cb22e3b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/CaseWhen.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/CaseWhen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/CountExpression.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/CountExpression.kt index bd3f24a36..11c5ec260 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/CountExpression.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/CountExpression.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt index fca3d6307..f30ce1b1a 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -195,7 +195,7 @@ public fun > Database.insertAndGenerateKey(table: T, block: Ass /** * Get generated key from the row set. */ -internal fun CachedRowSet.getGeneratedKey(primaryKey: Column): T? { +public fun CachedRowSet.getGeneratedKey(primaryKey: Column): T? { if (metaData.columnCount == 1) { return primaryKey.sqlType.getResult(this, 1) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Operators.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Operators.kt index 7461d99e1..152d6ce2d 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Operators.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Operators.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index 1bfd7eb12..652a18e91 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/QueryRowSet.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/QueryRowSet.kt index a13eff7e3..7c5b25f8b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/QueryRowSet.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/QueryRowSet.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/QuerySource.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/QuerySource.kt index 3c90e7e89..490d80ac6 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/QuerySource.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/QuerySource.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/WindowFunctions.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/WindowFunctions.kt index ba70a5320..0376e4547 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/WindowFunctions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/WindowFunctions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/DefaultMethodHandler.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/DefaultMethodHandler.kt index f92a07d73..184a0ad78 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/DefaultMethodHandler.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/DefaultMethodHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt index ce2d1cc62..2630ad24f 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt index 2e827d3b0..bfed33b71 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt index e2d341043..caf34f73f 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt index d3e4cf2b1..b0134fc3b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensionsApi.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityGrouping.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityGrouping.kt index 27165d38b..3e6e528e9 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityGrouping.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityGrouping.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index 7bbd8a082..a1b1f769e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index 608751092..080442cfa 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt index 5b3e12d92..e0e8f1a47 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Reflections.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt index 967b239ad..91f560165 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitorInterceptor.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitorInterceptor.kt index f89829ad7..f60f3ba0e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitorInterceptor.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitorInterceptor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index ddc1ad7d5..8e9701ed9 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 9d55740d4..a3db1b64e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/AndroidLoggerAdapter.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/AndroidLoggerAdapter.kt index f6f6532d5..acd993053 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/AndroidLoggerAdapter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/AndroidLoggerAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/CommonsLoggerAdapter.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/CommonsLoggerAdapter.kt index 8284631fb..aa711a3e9 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/CommonsLoggerAdapter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/CommonsLoggerAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/ConsoleLogger.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/ConsoleLogger.kt index 8771a0aff..22c5c77f5 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/ConsoleLogger.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/ConsoleLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/JdkLoggerAdapter.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/JdkLoggerAdapter.kt index 5eabaea4d..c0eff0dc0 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/JdkLoggerAdapter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/JdkLoggerAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/Logger.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/Logger.kt index 2a75dcf1b..aa51068ea 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/Logger.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/Logger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/NoOpLogger.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/NoOpLogger.kt index c741fec17..8e4e577eb 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/NoOpLogger.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/NoOpLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/logging/Slf4jLoggerAdapter.kt b/ktorm-core/src/main/kotlin/org/ktorm/logging/Slf4jLoggerAdapter.kt index f508a2331..037270123 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/logging/Slf4jLoggerAdapter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/logging/Slf4jLoggerAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt index 2ad21514e..b1f425b99 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -274,8 +274,9 @@ public abstract class BaseTable( stack.push(root.toString(withAlias = false)) if (tableName == root.tableName && catalog == root.catalog && schema == root.schema) { + val route = stack.asReversed().joinToString(separator = " --> ") throw IllegalStateException( - "Circular reference detected, current table: '$this', reference route: ${stack.asReversed()}" + "Circular reference detected, current table: '$this', reference route: $route" ) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt index 7fd907e99..6d4f80951 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/Column.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt index a8ddc2680..de2d6f4b6 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/RefCounter.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/RefCounter.kt index c712414ba..b7c29abe1 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/RefCounter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/RefCounter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlType.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlType.kt index 369dd5acc..97ff0d14f 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlType.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt index e033bf492..507a297b9 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -504,6 +504,9 @@ public inline fun > BaseTable<*>.enum(name: String): Column< * @property enumClass the enum class. */ public class EnumSqlType>(public val enumClass: Class) : SqlType(Types.OTHER, "enum") { + @Suppress("UNCHECKED_CAST") + public constructor(typeRef: TypeReference) : this(typeRef.referencedType as Class) + private val pgStatementClass = try { Class.forName("org.postgresql.PGStatement") } catch (_: ClassNotFoundException) { null } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt index 33588ab43..7ad6581c0 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/Table.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/TypeReference.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/TypeReference.kt index aa8441a1b..fe5ad5aeb 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/TypeReference.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/TypeReference.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt index 745d31b39..fdda2bd8f 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt @@ -116,4 +116,4 @@ abstract class BaseTest { val Database.employees get() = this.sequenceOf(Employees) val Database.customers get() = this.sequenceOf(Customers) -} \ No newline at end of file +} diff --git a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt index adcfe324f..ef3a3bf55 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt @@ -114,7 +114,7 @@ class DatabaseTest : BaseTest() { companion object { fun forCode(code: Int): Status { - return values().first { it.code == code } + return entries.first { it.code == code } } } } diff --git a/ktorm-global/ktorm-global.gradle.kts b/ktorm-global/ktorm-global.gradle.kts index 4ccd4f19f..5a4eed686 100644 --- a/ktorm-global/ktorm-global.gradle.kts +++ b/ktorm-global/ktorm-global.gradle.kts @@ -1,6 +1,9 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { diff --git a/ktorm-global/src/main/kotlin/org/ktorm/global/Aggregations.kt b/ktorm-global/src/main/kotlin/org/ktorm/global/Aggregations.kt index 17de9abec..b82c02b94 100644 --- a/ktorm-global/src/main/kotlin/org/ktorm/global/Aggregations.kt +++ b/ktorm-global/src/main/kotlin/org/ktorm/global/Aggregations.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-global/src/main/kotlin/org/ktorm/global/Dml.kt b/ktorm-global/src/main/kotlin/org/ktorm/global/Dml.kt index eb15d0771..5bc5024c7 100644 --- a/ktorm-global/src/main/kotlin/org/ktorm/global/Dml.kt +++ b/ktorm-global/src/main/kotlin/org/ktorm/global/Dml.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-global/src/main/kotlin/org/ktorm/global/EntitySequence.kt b/ktorm-global/src/main/kotlin/org/ktorm/global/EntitySequence.kt index ac2741b60..5cec7acc1 100644 --- a/ktorm-global/src/main/kotlin/org/ktorm/global/EntitySequence.kt +++ b/ktorm-global/src/main/kotlin/org/ktorm/global/EntitySequence.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-global/src/main/kotlin/org/ktorm/global/Global.kt b/ktorm-global/src/main/kotlin/org/ktorm/global/Global.kt index 2191a274f..c49a15ecb 100644 --- a/ktorm-global/src/main/kotlin/org/ktorm/global/Global.kt +++ b/ktorm-global/src/main/kotlin/org/ktorm/global/Global.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-global/src/main/kotlin/org/ktorm/global/Query.kt b/ktorm-global/src/main/kotlin/org/ktorm/global/Query.kt index 9f985e409..01f7e9fcc 100644 --- a/ktorm-global/src/main/kotlin/org/ktorm/global/Query.kt +++ b/ktorm-global/src/main/kotlin/org/ktorm/global/Query.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/ktorm-jackson.gradle.kts b/ktorm-jackson/ktorm-jackson.gradle.kts index dc55022aa..7827835c8 100644 --- a/ktorm-jackson/ktorm-jackson.gradle.kts +++ b/ktorm-jackson/ktorm-jackson.gradle.kts @@ -1,6 +1,9 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { @@ -10,7 +13,7 @@ dependencies { api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3") } -val generatedSourceDir = "${project.buildDir.absolutePath}/generated/source/main/kotlin" +val generatedSourceDir = "${project.layout.buildDirectory.asFile.get()}/generated/source/main/kotlin" val generatePackageVersion by tasks.registering(Copy::class) { from("src/main/kotlin/org/ktorm/jackson/PackageVersion.kt.tmpl") diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt index 3cfb580e2..6776f7764 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt index dd3b29a7c..aeab57ece 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityTypeResolverBuilder.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityTypeResolverBuilder.kt index b46a27a8c..6e9f171d2 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityTypeResolverBuilder.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityTypeResolverBuilder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt index 2f6d3bf81..43c4ea554 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt index ef0b90f3b..2006cdd5d 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -58,6 +58,12 @@ public class JsonSqlType( public val objectMapper: ObjectMapper, public val javaType: JavaType ) : SqlType(Types.OTHER, "json") { + public constructor(typeRef: TypeReference) : this(sharedObjectMapper, typeRef) + public constructor( + objectMapper: ObjectMapper, + typeRef: TypeReference + ) : this(objectMapper, objectMapper.constructType(typeRef.referencedType)) + // Access postgresql API by reflection, because it is not a JDK 9 module, // we are not able to require it in module-info.java. private val pgStatementClass = loadClass("org.postgresql.PGStatement") diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/KtormModule.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/KtormModule.kt index 38735777d..7c8e9ce4a 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/KtormModule.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/KtormModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/PackageVersion.kt.tmpl b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/PackageVersion.kt.tmpl index d5fc3977c..65e770734 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/PackageVersion.kt.tmpl +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/PackageVersion.kt.tmpl @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt b/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt index 7cd48d84b..66ebd5893 100644 --- a/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt +++ b/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2018-2023 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.jackson import com.fasterxml.jackson.annotation.JsonAlias diff --git a/ktorm-ksp-annotations/ktorm-ksp-annotations.gradle.kts b/ktorm-ksp-annotations/ktorm-ksp-annotations.gradle.kts new file mode 100644 index 000000000..bee31c74f --- /dev/null +++ b/ktorm-ksp-annotations/ktorm-ksp-annotations.gradle.kts @@ -0,0 +1,11 @@ + +plugins { + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") +} + +dependencies { + api(project(":ktorm-core")) +} diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Column.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Column.kt new file mode 100644 index 000000000..0eaa98484 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Column.kt @@ -0,0 +1,61 @@ +/* + * 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.SqlType +import kotlin.reflect.KClass + +/** + * Specify the mapped column for an entity property. If no `@Column` annotation is specified, the default values apply. + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +public annotation class Column( + + /** + * The name of the column. + * + * If not specified, the name will be generated by a naming strategy. This naming strategy can be configured + * by KSP option `ktorm.dbNamingStrategy`, which accepts the following values: + * + * - lower-snake-case (default): generate lower snake-case names, for example: userId --> user_id. + * + * - upper-snake-case: generate upper snake-case names, for example: userId --> USER_ID. + * + * - Class name of a custom naming strategy, which should be an implementation of + * `org.ktorm.ksp.spi.DatabaseNamingStrategy`. + */ + val name: String = "", + + /** + * The SQL type of the column. + * + * If not specified, the SQL type will be automatically inferred by the annotated property's Kotlin type. + * The specified class must be a Kotlin singleton object or a normal class with a constructor that accepts + * a single [org.ktorm.schema.TypeReference] argument. + */ + val sqlType: KClass> = Nothing::class, + + /** + * The name of the corresponding column property in the generated table class. + * + * If not specified, the name will be the annotated property's name. This behavior can be configured by KSP option + * `ktorm.codingNamingStrategy`, which accepts an implementation class name of + * `org.ktorm.ksp.spi.CodingNamingStrategy`. + */ + val propertyName: String = "", +) diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Ignore.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Ignore.kt new file mode 100644 index 000000000..f5d1cb322 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Ignore.kt @@ -0,0 +1,24 @@ +/* + * 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 + +/** + * Ignore the annotated property, not generating the column definition. + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +public annotation class Ignore diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/PrimaryKey.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/PrimaryKey.kt new file mode 100644 index 000000000..843a1c7d7 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/PrimaryKey.kt @@ -0,0 +1,24 @@ +/* + * 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 + +/** + * Mark the annotated column as a primary key. + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +public annotation class PrimaryKey diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/References.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/References.kt new file mode 100644 index 000000000..a0f2dd688 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/References.kt @@ -0,0 +1,64 @@ +/* + * 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 + +/** + * Specify the mapped column for an entity property, and bind this column to a reference table. Typically, + * this column is a foreign key in relational databases. Entity sequence APIs would automatically left-join + * all references (recursively) by default. + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +public annotation class References( + + /** + * The name of the column. + * + * If not specified, the name will be generated by a naming strategy. This naming strategy can be configured + * by KSP option `ktorm.dbNamingStrategy`, which accepts the following values: + * + * - lower-snake-case (default): generate names in lower snake-case, the names are concatenation of the current + * property's name and the referenced table's primary key name. For example, a property `user` referencing a table + * that has a primary key named `id` will get a name `user_id`. + * + * - upper-snake-case: generate names in upper snake-case, the names are concatenation of the current + * property's name and the referenced table's primary key name. For example, a property `user` referencing a table + * that has a primary key named `id` will get a name `USER_ID`. + * + * - Class name of a custom naming strategy, which should be an implementation of + * `org.ktorm.ksp.spi.DatabaseNamingStrategy`. + */ + val name: String = "", + + /** + * The name of the corresponding column property in the generated table class. + * + * If not specified, the name will be the concatenation of the current property's name and the referenced table's + * primary key name. This behavior can be configured by KSP option `ktorm.codingNamingStrategy`, which accepts + * an implementation class name of `org.ktorm.ksp.spi.CodingNamingStrategy`. + */ + val propertyName: String = "", + + /** + * The name of the corresponding referenced table property in the Refs wrapper class. + * + * If not specified, the name will be the annotated property's name. This behavior can be configured by KSP option + * `ktorm.codingNamingStrategy`, which accepts an implementation class name of + * `org.ktorm.ksp.spi.CodingNamingStrategy`. + */ + val refTablePropertyName: String = "", +) diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Table.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Table.kt new file mode 100644 index 000000000..457410424 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Table.kt @@ -0,0 +1,84 @@ +/* + * 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 + +/** + * Specify the table for an entity class. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +public annotation class Table( + + /** + * The name of the table. + * + * If not specified, the name will be generated by a naming strategy. This naming strategy can be configured + * by KSP option `ktorm.dbNamingStrategy`, which accepts the following values: + * + * - lower-snake-case (default): generate lower snake-case names, for example: UserProfile --> user_profile. + * + * - upper-snake-case: generate upper snake-case names, for example: UserProfile --> USER_PROFILE. + * + * - Class name of a custom naming strategy, which should be an implementation of + * `org.ktorm.ksp.spi.DatabaseNamingStrategy`. + */ + val name: String = "", + + /** + * The alias of the table. + */ + val alias: String = "", + + /** + * The catalog of the table. + * + * The default value can be configured by KSP option `ktorm.catalog`. + */ + val catalog: String = "", + + /** + * The schema of the table. + * + * The default value can be configured by KSP option `ktorm.schema`. + */ + val schema: String = "", + + /** + * The name of the corresponding table class in the generated code. + * + * If not specified, the name will be the plural form of the annotated class's name, + * for example: UserProfile --> UserProfiles. This behavior can be configured by KSP option + * `ktorm.codingNamingStrategy`, which accepts an implementation class name of + * `org.ktorm.ksp.spi.CodingNamingStrategy`. + */ + val className: String = "", + + /** + * The name of the corresponding entity sequence in the generated code. + * + * If not specified, the name will be the plural form of the annotated class's name, with the first word in + * lower case, for example: UserProfile --> userProfiles. This behavior can be configured by KSP option + * `ktorm.codingNamingStrategy`, which accepts an implementation class name of + * `org.ktorm.ksp.spi.CodingNamingStrategy`. + */ + val entitySequenceName: String = "", + + /** + * Specify properties that should be ignored for generating column definitions. + */ + val ignoreProperties: Array = [], +) diff --git a/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Undefined.kt b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Undefined.kt new file mode 100644 index 000000000..3b2f095ea --- /dev/null +++ b/ktorm-ksp-annotations/src/main/kotlin/org/ktorm/ksp/annotation/Undefined.kt @@ -0,0 +1,176 @@ +/* + * 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 sun.misc.Unsafe +import java.lang.reflect.Modifier +import java.lang.reflect.Proxy +import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentHashMap + +/** + * Utility class that creates unique `undefined` values for any class. + * + * These `undefined` values are typically used as default values of parameters in ktorm-ksp generated pseudo + * constructor functions for entities. Pseudo constructors check if the parameter is referential identical to + * the `undefined` value to judge whether it is manually assigned by users or not. We don't use `null` in + * this case because `null` can also be a valid value for entity properties. + * + * For example: + * + * ```kotlin + * public fun Employee(name: String? = Undefined.of()): Employee { + * val entity = Entity.create() + * if (name !== Undefined.of()) { + * entity.name = name + * } + * return entity + * } + * ``` + * + * In this example, `Employee("vince")` creates an employee named vince, `Employee(null)` creates an employee with + * null name (can also be valid in some cases), and, in the meanwhile, `Employee()` creates an employee without giving + * a name, which is different with `Employee(null)`. + * + * Note: `undefined` values created by this class can only be used for referential comparing, any method invocation + * on these values can cause exceptions. + */ +public object Undefined { + private val unsafe = getUnsafe() + private val undefinedValuesCache = ConcurrentHashMap, Any>() + + /** + * Return the `undefined` value for class [T]. See more details in the class level document. + * + * Note: this function never returns null, but we have to mark it as nullable to ensure we are always boxing + * JVM primitive types and Kotlin inline classes. + */ + public inline fun of(): T? { + return getUndefinedValue(T::class.java) as T? + } + + /** + * Get or create the `undefined` value for class [cls]. + */ + @PublishedApi + internal fun getUndefinedValue(cls: Class<*>): Any { + return undefinedValuesCache.computeIfAbsent(cls) { + if (cls.isArray) { + java.lang.reflect.Array.newInstance(cls.componentType, 0) + } else if (cls.isInterface) { + createUndefinedValueByJdkProxy(cls) + } else if (Modifier.isAbstract(cls.modifiers)) { + createUndefinedValueBySubclassing(cls) + } else { + unsafe.allocateInstance(cls) + } + } + } + + /** + * Obtain the [Unsafe] instance by reflection. + */ + private fun getUnsafe(): Unsafe { + val field = Unsafe::class.java.getDeclaredField("theUnsafe") + field.isAccessible = true + return field.get(null) as Unsafe + } + + /** + * Create the `undefined` value for interface by JDK dynamic proxy. + */ + private fun createUndefinedValueByJdkProxy(cls: Class<*>): Any { + return Proxy.newProxyInstance(cls.classLoader, arrayOf(cls)) { proxy, method, args -> + when (method.declaringClass.kotlin) { + Any::class -> { + when (method.name) { + "equals" -> proxy === args!![0] + "hashCode" -> System.identityHashCode(proxy) + "toString" -> "Ktorm undefined value proxy for ${cls.name}" + else -> throw UnsupportedOperationException("Method not supported: $method") + } + } + else -> { + throw UnsupportedOperationException("Method not supported: $method") + } + } + } + } + + /** + * Create the `undefined` value for abstract class by generating a subclass dynamically. + */ + private fun createUndefinedValueBySubclassing(cls: Class<*>): Any { + val subclassName = if (cls.name.startsWith("java.")) "\$${cls.name}\$Undefined" else "${cls.name}\$Undefined" + val classLoader = UndefinedClassLoader(cls.classLoader ?: Thread.currentThread().contextClassLoader) + val subclass = Class.forName(subclassName, true, classLoader) + return unsafe.allocateInstance(subclass) + } + + /** + * Class loader that generates `undefined` subclasses. + * + * Note: + * + * 1. Subclasses generated by this class loader doesn't have any constructors, so we have to use + * [Unsafe.allocateInstance] to create instances. + * + * 2. Subclasses generated by this class loader doesn't implement any abstract methods from their super classes, + * so any invocation on those abstract methods will cause [AbstractMethodError]. + */ + private class UndefinedClassLoader(parent: ClassLoader) : ClassLoader(parent) { + + override fun findClass(name: String): Class<*> { + if (!name.endsWith("\$Undefined")) { + throw ClassNotFoundException(name) + } + + val className = name.replace(".", "/") + val superClassName = className.removePrefix("\$").removeSuffix("\$Undefined") + val bytes = generateByteCode(className.toByteArray(), superClassName.toByteArray()) + return defineClass(name, bytes, null) + } + + @Suppress("MagicNumber", "NoMultipleSpaces") + private fun generateByteCode(className: ByteArray, superClassName: ByteArray): ByteBuffer { + val buf = ByteBuffer.allocate(1024) + buf.putInt(0xCAFEBABE.toInt()) // magic + buf.putShort(0) // minor version + buf.putShort(52) // major version, 52 for JDK1.8 + buf.putShort(5) // constant pool count, 5 means 4 constants in all + buf.put(1) // #1, CONSTANT_Utf8_info + buf.putShort(className.size.toShort()) // length + buf.put(className) // class name + buf.put(7) // #2, CONSTANT_Class_info + buf.putShort(1) // name index, ref to constant #1 + buf.put(1) // #3, CONSTANT_Utf8_info + buf.putShort(superClassName.size.toShort()) // length + buf.put(superClassName) // super class name + buf.put(7) // #4, CONSTANT_Class_info + buf.putShort(3) // name index, ref to constant #3 + buf.putShort((0x0001 or 0x0020 or 0x1000).toShort()) // ACC_PUBLIC | ACC_SUPER | ACC_SYNTHETIC + buf.putShort(2) // this class, ref to constant #2 + buf.putShort(4) // super class, ref to constant #4 + buf.putShort(0) // interfaces count + buf.putShort(0) // fields count + buf.putShort(0) // methods count + buf.putShort(0) // attributes count + buf.flip() + return buf + } + } +} diff --git a/ktorm-ksp-annotations/src/main/moditect/module-info.java b/ktorm-ksp-annotations/src/main/moditect/module-info.java new file mode 100644 index 000000000..698a72c04 --- /dev/null +++ b/ktorm-ksp-annotations/src/main/moditect/module-info.java @@ -0,0 +1,5 @@ +module ktorm.ksp.annotations { + requires ktorm.core; + requires jdk.unsupported; + exports org.ktorm.ksp.annotation; +} diff --git a/ktorm-ksp-annotations/src/test/kotlin/org/ktorm/ksp/annotation/UndefinedTest.kt b/ktorm-ksp-annotations/src/test/kotlin/org/ktorm/ksp/annotation/UndefinedTest.kt new file mode 100644 index 000000000..e636c9fa5 --- /dev/null +++ b/ktorm-ksp-annotations/src/test/kotlin/org/ktorm/ksp/annotation/UndefinedTest.kt @@ -0,0 +1,155 @@ +package org.ktorm.ksp.annotation + +import org.junit.Test +import org.ktorm.entity.Entity + +class UndefinedTest { + + private inline fun testUndefined(value: T?) { + val undefined1 = Undefined.of() + val undefined2 = Undefined.of() + + assert(undefined1 is T) + assert(undefined2 is T) + assert(undefined1 !== value) + assert(undefined2 !== value) + assert(undefined1 === undefined2) + + println("Undefined Class Name: " + undefined1!!.javaClass.name) + } + + private fun testUndefinedInt(haveValue: Boolean, value: Int? = Undefined.of()) { + val undefined = Undefined.of() + println("Undefined Class Name: " + undefined!!.javaClass.name) + + if (haveValue) { + assert(value !== undefined) + } else { + assert(value === undefined) + } + } + + private fun testUndefinedUInt(haveValue: Boolean, value: UInt? = Undefined.of()) { + val undefined = Undefined.of() + println("Undefined Class Name: " + undefined!!.javaClass.name) + + if (haveValue) { + assert((value as Any?) !== (undefined as Any?)) + } else { + assert((value as Any?) === (undefined as Any?)) + } + } + + @Test + fun `undefined inlined class`() { + testUndefinedUInt(haveValue = true, value = 1U) + testUndefinedUInt(haveValue = true, value = 0U) + testUndefinedUInt(haveValue = true, value = null) + testUndefinedUInt(haveValue = false) + + testUndefined(0.toUByte()) + testUndefined(0.toUShort()) + testUndefined(0U) + testUndefined(0UL) + } + + @Test + fun `undefined primitive type`() { + testUndefinedInt(haveValue = true, value = 1) + testUndefinedInt(haveValue = true, value = 0) + testUndefinedInt(haveValue = true, value = null) + testUndefinedInt(haveValue = false) + + testUndefined(0.toByte()) + testUndefined(0.toShort()) + testUndefined(0) + testUndefined(0L) + testUndefined('0') + testUndefined(false) + testUndefined(0f) + testUndefined(0.0) + } + + private interface Employee : Entity + + enum class Gender { + MALE, + FEMALE + } + + abstract class Biology + + @Suppress("unused") + abstract class Animal(val name: String) : Biology() + + @Suppress("unused") + private class Dog(val age: Int) : Animal("dog") + + private data class Cat(val age: Int) : Animal("cat") + + @Test + fun `undefined interface`() { + testUndefined(Entity.create()) + testUndefined(null) + } + + @Test + fun `undefined abstract class`() { + testUndefined(Dog(0)) + testUndefined(Dog(0)) + testUndefined(0) + } + + @Test + fun `undefined enum`() { + testUndefined(Gender.MALE) + testUndefined(Gender.FEMALE) + } + + @Test + fun `undefined class`() { + testUndefined(Dog(0)) + } + + @Test + fun `undefined data class`() { + testUndefined(Cat(0)) + } + + private class School { + inner class Teacher + + @Suppress("unused") + inner class Class(private val name: String) + } + + @Test + fun `undefined inner class`() { + val school = School() + val teacher = school.Teacher() + testUndefined(teacher) + val aClass = school.Class("A") + testUndefined(aClass) + } + + @Test + fun `undefined object`() { + testUndefined(Unit) + } + + @Test + fun `undefined companion object`() { + testUndefined(Int.Companion) + } + + @Test + fun `undefined function`() { + testUndefined<(Int) -> String> { it.toString() } + } + + @Test + fun `undefined array`() { + testUndefined(intArrayOf()) + testUndefined>(arrayOf()) + } +} diff --git a/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts b/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts new file mode 100644 index 000000000..3c2766dba --- /dev/null +++ b/ktorm-ksp-compiler-maven-plugin/ktorm-ksp-compiler-maven-plugin.gradle.kts @@ -0,0 +1,17 @@ + +plugins { + id("ktorm.base") + id("ktorm.publish") + id("ktorm.source-header-check") +} + +dependencies { + compileOnly(kotlin("maven-plugin")) + compileOnly(kotlin("compiler")) + compileOnly("org.apache.maven:maven-core:3.9.3") + implementation("com.google.devtools.ksp:symbol-processing-cmdline:1.9.0-1.0.13") + implementation(project(":ktorm-ksp-compiler")) { + exclude(group = "com.pinterest.ktlint", module = "ktlint-rule-engine") + exclude(group = "com.pinterest.ktlint", module = "ktlint-ruleset-standard") + } +} diff --git a/ktorm-ksp-compiler-maven-plugin/src/main/kotlin/org/ktorm/ksp/compiler/maven/KtormKspMavenPluginExtension.kt b/ktorm-ksp-compiler-maven-plugin/src/main/kotlin/org/ktorm/ksp/compiler/maven/KtormKspMavenPluginExtension.kt new file mode 100644 index 000000000..a99177273 --- /dev/null +++ b/ktorm-ksp-compiler-maven-plugin/src/main/kotlin/org/ktorm/ksp/compiler/maven/KtormKspMavenPluginExtension.kt @@ -0,0 +1,209 @@ +/* + * 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.compiler.maven + +import com.google.devtools.ksp.KspCliOption +import org.apache.maven.artifact.resolver.ArtifactResolutionRequest +import org.apache.maven.execution.MavenSession +import org.apache.maven.plugin.MojoExecution +import org.apache.maven.plugin.MojoExecutionException +import org.apache.maven.project.MavenProject +import org.apache.maven.repository.RepositorySystem +import org.codehaus.plexus.component.annotations.Component +import org.codehaus.plexus.component.annotations.Requirement +import org.jetbrains.kotlin.maven.KotlinMavenPluginExtension +import org.jetbrains.kotlin.maven.PluginOption +import java.io.File + +/** + * Extension that enables KSP for the kotlin maven plugin. + */ +@Component(role = KotlinMavenPluginExtension::class, hint = "ksp") +public class KtormKspMavenPluginExtension : KotlinMavenPluginExtension { + @Requirement + private lateinit var repositorySystem: RepositorySystem + + @Requirement + private lateinit var mavenSession: MavenSession + + override fun getCompilerPluginId(): String { + return "com.google.devtools.ksp.symbol-processing" + } + + override fun isApplicable(project: MavenProject, execution: MojoExecution): Boolean { + return execution.mojoDescriptor.goal == "compile" || execution.mojoDescriptor.goal == "test-compile" + } + + override fun getPluginOptions(project: MavenProject, execution: MojoExecution): List { + val userOptions = parseUserOptions(execution) + + if (execution.mojoDescriptor.goal == "compile") { + val options = buildPluginOptions(project, execution, userOptions) + for (key in listOf(KspCliOption.JAVA_OUTPUT_DIR_OPTION, KspCliOption.KOTLIN_OUTPUT_DIR_OPTION)) { + project.addCompileSourceRoot(options[key] ?: userOptions[key]!![0]) + } + + return options.map { (option, value) -> PluginOption("ksp", compilerPluginId, option.optionName, value) } + } + + if (execution.mojoDescriptor.goal == "test-compile") { + val options = buildTestPluginOptions(project, execution, userOptions) + for (key in listOf(KspCliOption.JAVA_OUTPUT_DIR_OPTION, KspCliOption.KOTLIN_OUTPUT_DIR_OPTION)) { + project.addTestCompileSourceRoot(options[key] ?: userOptions[key]!![0]) + } + + return options.map { (option, value) -> PluginOption("ksp", compilerPluginId, option.optionName, value) } + } + + return emptyList() + } + + private fun parseUserOptions(execution: MojoExecution): Map> { + val pluginOptions = execution.configuration.getChild("pluginOptions") ?: return emptyMap() + val availableOptions = KspCliOption.entries.associateBy { it.optionName } + val pattern = Regex("([^:]+):([^=]+)=(.*)") + + return pluginOptions.children + .mapNotNull { pattern.matchEntire(it.value) } + .map { it.destructured } + .filter { (plugin, key, value) -> plugin == "ksp" && key in availableOptions && value.isNotBlank() } + .groupBy({ (_, key, _) -> availableOptions[key]!! }, { (_, _, value) -> value }) + } + + private fun buildPluginOptions( + project: MavenProject, execution: MojoExecution, userOptions: Map> + ): Map { + val baseDir = project.basedir.path + val buildDir = project.build.directory + val options = LinkedHashMap() + + if (KspCliOption.CLASS_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.CLASS_OUTPUT_DIR_OPTION] = project.build.outputDirectory + } + if (KspCliOption.JAVA_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.JAVA_OUTPUT_DIR_OPTION] = path(buildDir, "generated-sources", "ksp-java") + } + if (KspCliOption.KOTLIN_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.KOTLIN_OUTPUT_DIR_OPTION] = path(buildDir, "generated-sources", "ksp") + } + if (KspCliOption.RESOURCE_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.RESOURCE_OUTPUT_DIR_OPTION] = project.build.outputDirectory + } + if (KspCliOption.CACHES_DIR_OPTION !in userOptions) { + options[KspCliOption.CACHES_DIR_OPTION] = path(buildDir, "ksp-caches") + } + if (KspCliOption.PROJECT_BASE_DIR_OPTION !in userOptions) { + options[KspCliOption.PROJECT_BASE_DIR_OPTION] = baseDir + } + if (KspCliOption.KSP_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.KSP_OUTPUT_DIR_OPTION] = path(buildDir, "ksp") + } + if (KspCliOption.PROCESSOR_CLASSPATH_OPTION !in userOptions) { + options[KspCliOption.PROCESSOR_CLASSPATH_OPTION] = processorClasspath(project, execution) + } + if (KspCliOption.WITH_COMPILATION_OPTION !in userOptions) { + options[KspCliOption.WITH_COMPILATION_OPTION] = "true" + } + + val apOptions = userOptions[KspCliOption.PROCESSING_OPTIONS_OPTION] ?: emptyList() + if (apOptions.none { it.startsWith("ktorm.ktlintExecutable=") }) { + options[KspCliOption.PROCESSING_OPTIONS_OPTION] = "ktorm.ktlintExecutable=${ktlintExecutable(project)}" + } + + return options + } + + private fun buildTestPluginOptions( + project: MavenProject, execution: MojoExecution, userOptions: Map> + ): Map { + val baseDir = project.basedir.path + val buildDir = project.build.directory + val options = LinkedHashMap() + + if (KspCliOption.CLASS_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.CLASS_OUTPUT_DIR_OPTION] = project.build.testOutputDirectory + } + if (KspCliOption.JAVA_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.JAVA_OUTPUT_DIR_OPTION] = path(buildDir, "generated-test-sources", "ksp-java") + } + if (KspCliOption.KOTLIN_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.KOTLIN_OUTPUT_DIR_OPTION] = path(buildDir, "generated-test-sources", "ksp") + } + if (KspCliOption.RESOURCE_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.RESOURCE_OUTPUT_DIR_OPTION] = project.build.testOutputDirectory + } + if (KspCliOption.CACHES_DIR_OPTION !in userOptions) { + options[KspCliOption.CACHES_DIR_OPTION] = path(buildDir, "ksp-caches") + } + if (KspCliOption.PROJECT_BASE_DIR_OPTION !in userOptions) { + options[KspCliOption.PROJECT_BASE_DIR_OPTION] = baseDir + } + if (KspCliOption.KSP_OUTPUT_DIR_OPTION !in userOptions) { + options[KspCliOption.KSP_OUTPUT_DIR_OPTION] = path(buildDir, "ksp-test") + } + if (KspCliOption.PROCESSOR_CLASSPATH_OPTION !in userOptions) { + options[KspCliOption.PROCESSOR_CLASSPATH_OPTION] = processorClasspath(project, execution) + } + if (KspCliOption.WITH_COMPILATION_OPTION !in userOptions) { + options[KspCliOption.WITH_COMPILATION_OPTION] = "true" + } + + val apOptions = userOptions[KspCliOption.PROCESSING_OPTIONS_OPTION] ?: emptyList() + if (apOptions.none { it.startsWith("ktorm.ktlintExecutable=") }) { + options[KspCliOption.PROCESSING_OPTIONS_OPTION] = "ktorm.ktlintExecutable=${ktlintExecutable(project)}" + } + + return options + } + + private fun processorClasspath(project: MavenProject, execution: MojoExecution): String { + val files = ArrayList() + for (dependency in execution.plugin.dependencies) { + val r = ArtifactResolutionRequest() + r.artifact = repositorySystem.createDependencyArtifact(dependency) + r.localRepository = mavenSession.localRepository + r.remoteRepositories = project.pluginArtifactRepositories + r.isResolveTransitively = true + + val resolved = repositorySystem.resolve(r) + files += resolved.artifacts.mapNotNull { it.file }.filter { it.exists() } + } + + return files.joinToString(File.pathSeparator) { it.path } + } + + private fun ktlintExecutable(project: MavenProject): String { + val r = ArtifactResolutionRequest() + r.artifact = repositorySystem.createArtifactWithClassifier("com.pinterest", "ktlint", "0.50.0", "jar", "all") + r.localRepository = mavenSession.localRepository + r.remoteRepositories = project.pluginArtifactRepositories + r.isResolveTransitively = false + + val resolved = repositorySystem.resolve(r) + val file = resolved.artifacts.mapNotNull { it.file }.firstOrNull { it.exists() } + if (file != null) { + return file.path + } else { + throw MojoExecutionException("Resolve ktlint executable jar failed.") + } + } + + private fun path(parent: String, vararg children: String): String { + val file = children.fold(File(parent)) { acc, child -> File(acc, child) } + return file.path + } +} diff --git a/ktorm-ksp-compiler-maven-plugin/src/main/resources/META-INF/plexus/components.xml b/ktorm-ksp-compiler-maven-plugin/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 000000000..3571e922a --- /dev/null +++ b/ktorm-ksp-compiler-maven-plugin/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,21 @@ + + + + + org.jetbrains.kotlin.maven.KotlinMavenPluginExtension + ksp + org.ktorm.ksp.compiler.maven.KtormKspMavenPluginExtension + false + + + org.apache.maven.repository.RepositorySystem + repositorySystem + + + org.apache.maven.execution.MavenSession + mavenSession + + + + + diff --git a/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts b/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts new file mode 100644 index 000000000..2285feb5e --- /dev/null +++ b/ktorm-ksp-compiler/ktorm-ksp-compiler.gradle.kts @@ -0,0 +1,32 @@ + +plugins { + id("ktorm.base") + id("ktorm.publish") + id("ktorm.source-header-check") +} + +dependencies { + implementation(project(":ktorm-core")) + implementation(project(":ktorm-ksp-annotations")) + implementation(project(":ktorm-ksp-spi")) + implementation("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13") + implementation("com.squareup:kotlinpoet-ksp:1.11.0") + implementation("org.atteo:evo-inflector:1.3") + implementation("com.pinterest.ktlint:ktlint-rule-engine:0.50.0") { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-compiler-embeddable") + } + implementation("com.pinterest.ktlint:ktlint-ruleset-standard:0.50.0") { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-compiler-embeddable") + } + + testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.5.0") + testImplementation("com.github.tschuchortdev:kotlin-compile-testing-ksp:1.5.0") + testImplementation("com.h2database:h2:1.4.198") + testImplementation("org.slf4j:slf4j-simple:2.0.3") +} + +if (JavaVersion.current() >= JavaVersion.VERSION_1_9) { + tasks.test { + jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/KtormProcessorProvider.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/KtormProcessorProvider.kt new file mode 100644 index 000000000..2679fc0d2 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/KtormProcessorProvider.kt @@ -0,0 +1,111 @@ +/* + * 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.compiler + +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import org.ktorm.ksp.annotation.Table +import org.ktorm.ksp.compiler.formatter.CodeFormatter +import org.ktorm.ksp.compiler.formatter.KtLintCodeFormatter +import org.ktorm.ksp.compiler.formatter.StandaloneKtLintCodeFormatter +import org.ktorm.ksp.compiler.generator.FileGenerator +import org.ktorm.ksp.compiler.parser.MetadataParser +import org.ktorm.ksp.compiler.util.isValid +import org.ktorm.ksp.spi.TableMetadata +import kotlin.reflect.jvm.jvmName + +/** + * Ktorm KSP symbol processor provider. + */ +public class KtormProcessorProvider : SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + for (generator in FileGenerator.extCodeGenerators) { + environment.logger.info("[ktorm-ksp-compiler] load ext generator: $generator") + } + + return object : SymbolProcessor { + override fun process(resolver: Resolver): List { + return doProcess(resolver, environment) + } + } + } + + private fun doProcess(resolver: Resolver, environment: SymbolProcessorEnvironment): List { + val (symbols, deferral) = resolver.getSymbolsWithAnnotation(Table::class.jvmName).partition { it.isValid() } + if (symbols.isNotEmpty()) { + val parser = MetadataParser(resolver, environment) + val formatter = getCodeFormatter(environment) + + for (symbol in symbols) { + if (symbol !is KSClassDeclaration) { + continue + } + + // Parse table metadata from the symbol. + val table = parser.parseTableMetadata(symbol) + + // Generate file spec by kotlinpoet. + val fileSpec = FileGenerator.generate(table, environment) + + // Beautify the generated code. + val formattedCode = formatter.format(fileSpec.toString()) + + // Output the formatted code. + val dependencies = Dependencies(false, *table.getDependencyFiles().toTypedArray()) + val file = environment.codeGenerator.createNewFile(dependencies, fileSpec.packageName, fileSpec.name) + file.bufferedWriter(Charsets.UTF_8).use { it.write(formattedCode) } + } + } + + return deferral + } + + private fun getCodeFormatter(environment: SymbolProcessorEnvironment): CodeFormatter { + if (!environment.options["ktorm.ktlintExecutable"].isNullOrBlank()) { + return StandaloneKtLintCodeFormatter(environment) + } + + try { + return KtLintCodeFormatter(environment) + } catch (_: ClassNotFoundException) { + } catch (_: NoClassDefFoundError) { + } + + return CodeFormatter { code -> code } + } + + private fun TableMetadata.getDependencyFiles(): List { + val files = ArrayList() + + val containingFile = entityClass.containingFile + if (containingFile != null) { + files += containingFile + } + + for (column in columns) { + val ref = column.referenceTable + if (ref != null) { + files += ref.getDependencyFiles() + } + } + + return files + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/CodeFormatter.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/CodeFormatter.kt new file mode 100644 index 000000000..993b3b5f8 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/CodeFormatter.kt @@ -0,0 +1,28 @@ +/* + * 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.compiler.formatter + +/** + * Code formatter interface. + */ +internal fun interface CodeFormatter { + + /** + * Format the generated code to the community recommended coding style. + */ + fun format(code: String): String +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/KtLintCodeFormatter.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/KtLintCodeFormatter.kt new file mode 100644 index 000000000..1e4501bc3 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/KtLintCodeFormatter.kt @@ -0,0 +1,59 @@ +/* + * 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.compiler.formatter + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 +import com.pinterest.ktlint.rule.engine.api.Code +import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine +import org.ec4j.core.EditorConfigLoader +import org.ec4j.core.Resource.Resources +import java.util.* + +internal class KtLintCodeFormatter(val environment: SymbolProcessorEnvironment) : CodeFormatter { + private val ktLintRuleEngine = KtLintRuleEngine( + ruleProviders = ServiceLoader + .load(RuleSetProviderV3::class.java, javaClass.classLoader) + .flatMap { it.getRuleProviders() } + .toSet(), + editorConfigDefaults = EditorConfigDefaults( + EditorConfigLoader.default_().load( + Resources.ofClassPath(javaClass.classLoader, "/ktorm-ksp-compiler/.editorconfig", Charsets.UTF_8) + ) + ) + ) + + override fun format(code: String): String { + try { + // Manually fix some code styles before formatting. + val snippet = code + .replace(Regex("""\(\s*"""), "(") + .replace(Regex("""\s*\)"""), ")") + .replace(Regex(""",\s*"""), ", ") + .replace(Regex(""",\s*\)"""), ")") + .replace(Regex("""\s+get\(\)\s="""), " get() =") + .replace(Regex("""\s+=\s+"""), " = ") + .replace("import org.ktorm.ksp.`annotation`", "import org.ktorm.ksp.annotation") + + return ktLintRuleEngine.format(Code.fromSnippet(snippet)) + } catch (e: Throwable) { + environment.logger.exception(e) + return code + } + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/StandaloneKtLintCodeFormatter.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/StandaloneKtLintCodeFormatter.kt new file mode 100644 index 000000000..471b4e8f2 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/formatter/StandaloneKtLintCodeFormatter.kt @@ -0,0 +1,108 @@ +/* + * 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.compiler.formatter + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import java.io.File + +internal class StandaloneKtLintCodeFormatter(val environment: SymbolProcessorEnvironment) : CodeFormatter { + private val command = buildCommand() + + init { + environment.logger.info("[ktorm-ksp-compiler] init ktlint formatter with command: ${command.joinToString(" ")}") + } + + override fun format(code: String): String { + try { + val p = ProcessBuilder(command).start() + p.outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(preprocessCode(code)) } + p.waitFor() + + val formattedCode = p.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + if (p.exitValue() == 0) { + // Exit normally. + return formattedCode + } else { + if (formattedCode.isNotBlank()) { + // Some violations exist but the code is still formatted. + return formattedCode + } else { + // Exit exceptionally. + val msg = p.errorStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + environment.logger.error("[ktorm-ksp-compiler] ktlint exit with code: ${p.exitValue()}\n$msg") + return code + } + } + } catch (e: Throwable) { + environment.logger.exception(e) + return code + } + } + + private fun buildCommand(): List { + val n = "java.lang.reflect.InaccessibleObjectException" + val isJava8 = try { Class.forName(n); false } catch (_: ClassNotFoundException) { true } + + val java = findJavaExecutable() + val ktlint = environment.options["ktorm.ktlintExecutable"]!! + val config = createEditorConfigFile() + + if (isJava8) { + return listOf(java, "-jar", ktlint, "-F", "--stdin", "--log-level=none", "--editorconfig=$config") + } else { + val jvmArgs = arrayOf("--add-opens", "java.base/java.lang=ALL-UNNAMED") + return listOf(java, *jvmArgs, "-jar", ktlint, "-F", "--stdin", "--log-level=none", "--editorconfig=$config") + } + } + + private fun findJavaExecutable(): String { + var file = File(File(System.getProperty("java.home"), "bin"), "java") + if (!file.exists()) { + file = File(File(System.getProperty("java.home"), "bin"), "java.exe") + } + + if (file.exists()) { + return file.path + } else { + throw IllegalStateException("Could not find java executable.") + } + } + + private fun createEditorConfigFile(): String { + val file = File.createTempFile("ktlint", ".editorconfig") + file.deleteOnExit() + + file.outputStream().use { output -> + javaClass.classLoader.getResourceAsStream("ktorm-ksp-compiler/.editorconfig")!!.use { input -> + input.copyTo(output) + } + } + + return file.path + } + + private fun preprocessCode(code: String): String { + return code + .replace(Regex("""\(\s*"""), "(") + .replace(Regex("""\s*\)"""), ")") + .replace(Regex(""",\s*"""), ", ") + .replace(Regex(""",\s*\)"""), ")") + .replace(Regex("""\s+get\(\)\s="""), " get() =") + .replace(Regex("""\s+=\s+"""), " = ") + .replace("import org.ktorm.ksp.`annotation`", "import org.ktorm.ksp.annotation") + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/AddFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/AddFunctionGenerator.kt new file mode 100644 index 000000000..4952f024a --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/AddFunctionGenerator.kt @@ -0,0 +1,222 @@ +/* + * 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.compiler.generator + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import org.ktorm.dsl.AliasRemover +import org.ktorm.entity.EntitySequence +import org.ktorm.expression.ColumnAssignmentExpression +import org.ktorm.expression.InsertExpression +import org.ktorm.ksp.spi.ColumnMetadata +import org.ktorm.ksp.spi.TableMetadata +import org.ktorm.schema.Column + +@OptIn(KotlinPoetKspPreview::class) +internal object AddFunctionGenerator { + + fun generate(table: TableMetadata): FunSpec { + val primaryKeys = table.columns.filter { it.isPrimaryKey } + val useGeneratedKey = primaryKeys.size == 1 && primaryKeys[0].entityProperty.isMutable + val entityClass = table.entityClass.toClassName() + val tableClass = ClassName(table.entityClass.packageName.asString(), table.tableClassName) + + return FunSpec.builder("add") + .addKdoc(kdoc(table, useGeneratedKey)) + .receiver(EntitySequence::class.asClassName().parameterizedBy(entityClass, tableClass)) + .addParameters(parameters(entityClass, useGeneratedKey)) + .returns(Int::class.asClassName()) + .addCode(checkForDml()) + .addCode(addValFun(table, useGeneratedKey)) + .addCode(addAssignments(table)) + .addCode(createExpression()) + .addCode(executeUpdate(useGeneratedKey, primaryKeys)) + .build() + } + + private fun kdoc(table: TableMetadata, useGeneratedKey: Boolean): String { + if (useGeneratedKey) { + val pk = table.columns.single { it.isPrimaryKey } + val pkName = table.entityClass.simpleName.asString() + "." + pk.entityProperty.simpleName.asString() + return """ + Insert the given entity into the table that the sequence object represents. + + @param entity the entity to be inserted. + @param isDynamic whether only non-null columns should be inserted. + @param useGeneratedKey whether to obtain the generated primary key value and fill it into the property [$pkName] after insertion. + @return the affected record number. + """.trimIndent() + } else { + return """ + Insert the given entity into the table that the sequence object represents. + + @param entity the entity to be inserted. + @param isDynamic whether only non-null columns should be inserted. + @return the affected record number. + """.trimIndent() + } + } + + private fun parameters(entityClass: ClassName, useGeneratedKey: Boolean): List { + if (useGeneratedKey) { + return listOf( + ParameterSpec.builder("entity", entityClass).build(), + ParameterSpec.builder("isDynamic", typeNameOf()).defaultValue("false").build(), + ParameterSpec.builder("useGeneratedKey", typeNameOf()).defaultValue("false").build() + ) + } else { + return listOf( + ParameterSpec.builder("entity", entityClass).build(), + ParameterSpec.builder("isDynamic", typeNameOf()).defaultValue("false").build() + ) + } + } + + internal fun checkForDml(): CodeBlock { + val code = """ + val isModified = expression.where != null + || expression.groupBy.isNotEmpty() + || expression.having != null + || expression.isDistinct + || expression.orderBy.isNotEmpty() + || expression.offset != null + || expression.limit != null + if (isModified) { + val msg = "" + + "Entity manipulation functions are not supported by this sequence object. " + + "Please call on the origin sequence returned from database.sequenceOf(table)" + throw UnsupportedOperationException(msg) + } + + + """.trimIndent() + + return CodeBlock.of(code) + } + + internal fun addValFun(table: TableMetadata, useGeneratedKey: Boolean): CodeBlock { + if (useGeneratedKey) { + val pk = table.columns.single { it.isPrimaryKey } + val code = """ + fun MutableList<%1T<*>>.addVal(column: %2T, value: T?) { + if (useGeneratedKey && column === sourceTable.%3N) { + return + } + + if (isDynamic && value == null) { + return + } + + this += %1T(column.asExpression(), column.wrapArgument(value)) + } + + + """.trimIndent() + return CodeBlock.of( + code, + ColumnAssignmentExpression::class.asClassName(), + Column::class.asClassName(), + pk.columnPropertyName + ) + } else { + val code = """ + fun MutableList<%1T<*>>.addVal(column: %2T, value: T?) { + if (isDynamic && value == null) { + return + } + + this += %1T(column.asExpression(), column.wrapArgument(value)) + } + + + """.trimIndent() + return CodeBlock.of(code, ColumnAssignmentExpression::class.asClassName(), Column::class.asClassName()) + } + } + + private fun addAssignments(table: TableMetadata): CodeBlock { + return buildCodeBlock { + addStatement("val assignments = ArrayList<%T<*>>()", ColumnAssignmentExpression::class.asClassName()) + + for (column in table.columns) { + addStatement( + "assignments.addVal(sourceTable.%N, entity.%N)", + column.columnPropertyName, + column.entityProperty.simpleName.asString() + ) + } + + beginControlFlow("if (assignments.isEmpty())") + addStatement("return 0") + endControlFlow() + + add("\n") + } + } + + private fun createExpression(): CodeBlock { + return buildCodeBlock { + addStatement( + "val visitor = database.dialect.createExpressionVisitor(%T)", + AliasRemover::class.asClassName() + ) + addStatement( + "val expression = visitor.visit(%T(sourceTable.asExpression(), assignments))", + InsertExpression::class.asClassName() + ) + } + } + + private fun executeUpdate(useGeneratedKey: Boolean, primaryKeys: List): CodeBlock { + return buildCodeBlock { + if (!useGeneratedKey) { + addStatement("return database.executeUpdate(expression)") + } else { + beginControlFlow("if (!useGeneratedKey)") + addStatement("return database.executeUpdate(expression)") + nextControlFlow("else") + addNamed( + format = """ + val (effects, rowSet) = database.executeUpdateAndRetrieveKeys(expression) + if (rowSet.next()) { + val generatedKey = rowSet.%getGeneratedKey:M(sourceTable.%columnPropertyName:N) + if (generatedKey != null) { + if (database.logger.isDebugEnabled()) { + database.logger.debug("Generated Key: ${'$'}generatedKey") + } + + entity.%propertyName:N = generatedKey + } + } + + return effects + + """.trimIndent(), + + arguments = mapOf( + "propertyName" to primaryKeys[0].entityProperty.simpleName.asString(), + "columnPropertyName" to primaryKeys[0].columnPropertyName, + "getGeneratedKey" to MemberName("org.ktorm.dsl", "getGeneratedKey", true) + ) + ) + endControlFlow() + } + } + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt new file mode 100644 index 000000000..0846cd735 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGenerator.kt @@ -0,0 +1,46 @@ +/* + * 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.compiler.generator + +import com.google.devtools.ksp.isAbstract +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import org.ktorm.ksp.compiler.util._type +import org.ktorm.ksp.spi.TableMetadata + +@OptIn(KotlinPoetKspPreview::class) +internal object ComponentFunctionGenerator { + + fun generate(table: TableMetadata): Sequence { + return table.entityClass.getAllProperties() + .filter { it.isAbstract() } + .filterNot { it.simpleName.asString() in setOf("entityClass", "properties") } + .mapIndexed { i, prop -> + FunSpec.builder("component${i + 1}") + .addKdoc("Return the value of [%L.%L]. ", + table.entityClass.simpleName.asString(), prop.simpleName.asString()) + .addModifiers(KModifier.OPERATOR) + .receiver(table.entityClass.toClassName()) + .returns(prop._type.toTypeName()) + .addCode("return·this.%N", prop.simpleName.asString()) + .build() + } + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/CopyFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/CopyFunctionGenerator.kt new file mode 100644 index 000000000..c26e32250 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/CopyFunctionGenerator.kt @@ -0,0 +1,40 @@ +/* + * 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.compiler.generator + +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import org.ktorm.ksp.spi.TableMetadata + +@OptIn(KotlinPoetKspPreview::class) +internal object CopyFunctionGenerator { + + fun generate(table: TableMetadata): FunSpec { + val kdoc = "" + + "Return a deep copy of this entity (which has the same property values and tracked statuses), " + + "and alter the specified property values. " + + return FunSpec.builder("copy") + .addKdoc(kdoc) + .receiver(table.entityClass.toClassName()) + .addParameters(PseudoConstructorFunctionGenerator.buildParameters(table).asIterable()) + .returns(table.entityClass.toClassName()) + .addCode(PseudoConstructorFunctionGenerator.buildFunctionBody(table, isCopy = true)) + .build() + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/EntitySequencePropertyGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/EntitySequencePropertyGenerator.kt new file mode 100644 index 000000000..c2c7331ff --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/EntitySequencePropertyGenerator.kt @@ -0,0 +1,47 @@ +/* + * 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.compiler.generator + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import org.ktorm.database.Database +import org.ktorm.entity.EntitySequence +import org.ktorm.ksp.spi.TableMetadata + +@OptIn(KotlinPoetKspPreview::class) +internal object EntitySequencePropertyGenerator { + + fun generate(table: TableMetadata): PropertySpec { + val entityClass = table.entityClass.toClassName() + val tableClass = ClassName(table.entityClass.packageName.asString(), table.tableClassName) + val entitySequenceType = EntitySequence::class.asClassName().parameterizedBy(entityClass, tableClass) + + return PropertySpec.builder(table.entitySequenceName, entitySequenceType) + .addKdoc("Return the default entity sequence of [%L].", table.tableClassName) + .receiver(Database::class.asClassName()) + .getter( + FunSpec.getterBuilder() + .addStatement( + "return·this.%M(%N)", + MemberName("org.ktorm.entity", "sequenceOf", true), + table.tableClassName) + .build()) + .build() + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/FileGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/FileGenerator.kt new file mode 100644 index 000000000..2630b3086 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/FileGenerator.kt @@ -0,0 +1,85 @@ +/* + * 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.compiler.generator + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.ClassKind +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FileSpec +import org.ktorm.ksp.spi.ExtCodeGenerator +import org.ktorm.ksp.spi.TableMetadata +import java.util.* + +internal object FileGenerator { + val extCodeGenerators = ServiceLoader.load(ExtCodeGenerator::class.java, javaClass.classLoader).toList() + + fun generate(table: TableMetadata, environment: SymbolProcessorEnvironment): FileSpec { + val fileName = table.entityClass.packageName.asString().replace('.', '/') + "/" + table.tableClassName + ".kt" + environment.logger.info("[ktorm-ksp-compiler] generate file: $fileName") + + val fileSpec = FileSpec.builder(table.entityClass.packageName.asString(), table.tableClassName) + .addFileComment("Auto-generated by ktorm-ksp-compiler, DO NOT EDIT!") + .addAnnotation( + AnnotationSpec.builder(Suppress::class).addMember("%S", "RedundantVisibilityModifier").build()) + .addType(TableClassGenerator.generate(table, environment)) + .addProperty(EntitySequencePropertyGenerator.generate(table)) + + if (table.entityClass.classKind == ClassKind.INTERFACE) { + if (table.columns.any { it.isReference }) { + fileSpec.addProperty(RefsPropertyGenerator.generate(table)) + fileSpec.addType(RefsClassGenerator.generate(table)) + } + + fileSpec.addFunction(PseudoConstructorFunctionGenerator.generate(table)) + fileSpec.addFunction(CopyFunctionGenerator.generate(table)) + + for (func in ComponentFunctionGenerator.generate(table)) { + fileSpec.addFunction(func) + } + } else { + fileSpec.addFunction(AddFunctionGenerator.generate(table)) + + if (table.columns.any { it.isPrimaryKey }) { + fileSpec.addFunction(UpdateFunctionGenerator.generate(table)) + } + } + + for (generator in extCodeGenerators) { + val desc = "Code generated by ext generator: $generator" + + for (type in generator.generateTypes(table, environment)) { + fileSpec.addType( + type.toBuilder().addKdoc(if (type.kdoc.isEmpty()) desc else "\n\n$desc").build() + ) + } + + for (property in generator.generateProperties(table, environment)) { + fileSpec.addProperty( + property.toBuilder().addKdoc(if (property.kdoc.isEmpty()) desc else "\n\n$desc").build() + ) + } + + for (function in generator.generateFunctions(table, environment)) { + fileSpec.addFunction( + function.toBuilder().addKdoc(if (function.kdoc.isEmpty()) desc else "\n\n$desc").build() + ) + } + } + + return fileSpec.build() + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt new file mode 100644 index 000000000..046467ced --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGenerator.kt @@ -0,0 +1,99 @@ +/* + * 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.compiler.generator + +import com.google.devtools.ksp.isAbstract +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import org.ktorm.entity.Entity +import org.ktorm.ksp.annotation.Undefined +import org.ktorm.ksp.compiler.util.* +import org.ktorm.ksp.spi.TableMetadata + +@OptIn(KotlinPoetKspPreview::class) +internal object PseudoConstructorFunctionGenerator { + + fun generate(table: TableMetadata): FunSpec { + val kdoc = "" + + "Create an entity of [%L] and specify the initial values for each property, " + + "properties that doesn't have an initial value will leave unassigned. " + + return FunSpec.builder(table.entityClass.simpleName.asString()) + .addKdoc(kdoc, table.entityClass.simpleName.asString()) + .addParameters(buildParameters(table).asIterable()) + .returns(table.entityClass.toClassName()) + .addCode(buildFunctionBody(table)) + .build() + } + + internal fun buildParameters(table: TableMetadata): Sequence { + return table.entityClass.getAllProperties() + .filter { it.isAbstract() } + .filterNot { it.simpleName.asString() in setOf("entityClass", "properties") } + .map { prop -> + val propName = prop.simpleName.asString() + val propType = prop._type.makeNullable().toTypeName() + + ParameterSpec.builder(propName, propType) + .defaultValue("%T.of()", Undefined::class.asClassName()) + .build() + } + } + + internal fun buildFunctionBody(table: TableMetadata, isCopy: Boolean = false): CodeBlock = buildCodeBlock { + if (isCopy) { + addStatement("val·entity·=·this.copy()") + } else { + addStatement("val·entity·=·%T.create<%T>()", Entity::class.asClassName(), table.entityClass.toClassName()) + } + + for (prop in table.entityClass.getAllProperties()) { + if (!prop.isAbstract() || prop.simpleName.asString() in setOf("entityClass", "properties")) { + continue + } + + val condition: String + if (prop._type.isInline()) { + condition = "if·((%N·as·Any?)·!==·(%T.of<%T>()·as·Any?))" + } else { + condition = "if·(%N·!==·%T.of<%T>())" + } + + beginControlFlow(condition, + prop.simpleName.asString(), Undefined::class.asClassName(), prop._type.makeNotNullable().toTypeName() + ) + + var statement: String + if (prop.isMutable) { + statement = "entity.%1N·=·%1N" + } else { + statement = "entity[%1S]·=·%1N" + } + + if (!prop._type.isMarkedNullable) { + statement += "·?:·error(\"`%1L` should not be null.\")" + } + + addStatement(statement, prop.simpleName.asString()) + endControlFlow() + } + + addStatement("return entity") + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/RefsClassGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/RefsClassGenerator.kt new file mode 100644 index 000000000..7a2f7d714 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/RefsClassGenerator.kt @@ -0,0 +1,60 @@ +/* + * 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.compiler.generator + +import com.squareup.kotlinpoet.* +import org.ktorm.ksp.spi.TableMetadata +import org.ktorm.schema.Table + +/** + * Created by vince at Jul 15, 2023. + */ +internal object RefsClassGenerator { + + fun generate(table: TableMetadata): TypeSpec { + val tableClass = ClassName(table.entityClass.packageName.asString(), table.tableClassName) + + val typeSpec = TypeSpec.classBuilder("${table.tableClassName}Refs") + .addKdoc("Wrapper class that provides a convenient way to access referenced tables.") + .primaryConstructor(FunSpec.constructorBuilder().addParameter("t", tableClass).build()) + + for (column in table.columns) { + val refTable = column.referenceTable ?: continue + val refTableClass = ClassName(refTable.entityClass.packageName.asString(), refTable.tableClassName) + + val propertySpec = PropertySpec.builder(column.refTablePropertyName!!, refTableClass) + .addKdoc("Return the referenced table of [${table.tableClassName}.${column.columnPropertyName}].") + .initializer(CodeBlock.of("t.%N.referenceTable as %T", column.columnPropertyName, refTableClass)) + .build() + + typeSpec.addProperty(propertySpec) + } + + val funSpec = FunSpec.builder("toList") + .addKdoc("Return all referenced tables as a list.") + .returns(typeNameOf>>()) + .addCode("return·listOf(") + + for (column in table.columns) { + val refTablePropertyName = column.refTablePropertyName ?: continue + funSpec.addCode("%N, ", refTablePropertyName) + } + + typeSpec.addFunction(funSpec.addCode(")").build()) + return typeSpec.build() + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/RefsPropertyGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/RefsPropertyGenerator.kt new file mode 100644 index 000000000..2cb815b40 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/RefsPropertyGenerator.kt @@ -0,0 +1,39 @@ +/* + * 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.compiler.generator + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import org.ktorm.ksp.spi.TableMetadata + +/** + * Created by vince at Jul 15, 2023. + */ +internal object RefsPropertyGenerator { + + fun generate(table: TableMetadata): PropertySpec { + val tableClass = ClassName(table.entityClass.packageName.asString(), table.tableClassName) + val refsClass = ClassName(table.entityClass.packageName.asString(), "${table.tableClassName}Refs") + + return PropertySpec.builder("refs", refsClass) + .addKdoc("Return the refs object that provides a convenient way to access referenced tables.") + .receiver(tableClass) + .getter(FunSpec.getterBuilder().addStatement("return·%T(this)", refsClass).build()) + .build() + } +} 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 new file mode 100644 index 000000000..98609c1e8 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/TableClassGenerator.kt @@ -0,0 +1,235 @@ +/* + * 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.compiler.generator + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSValueParameter +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import org.ktorm.dsl.QueryRowSet +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 { + + fun generate(table: TableMetadata, environment: SymbolProcessorEnvironment): TypeSpec { + return TypeSpec.classBuilder(table.tableClassName) + .addKdoc("Table %L. %L", table.name, table.entityClass.docString?.trimIndent().orEmpty()) + .addModifiers(KModifier.OPEN) + .primaryConstructor(FunSpec.constructorBuilder().addParameter("alias", typeNameOf()).build()) + .configureSuperClass(table) + .configureColumnProperties(table) + .configureDoCreateEntityFunction(table, environment.options) + .configureAliasedFunction(table) + .configureCompanionObject(table) + .build() + } + + 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())) + } + + addSuperclassConstructorParameter("%S", table.name) + addSuperclassConstructorParameter("alias") + + if (table.catalog != null) { + addSuperclassConstructorParameter("catalog·=·%S", table.catalog!!) + } + + if (table.schema != null) { + addSuperclassConstructorParameter("schema·=·%S", table.schema!!) + } + + return this + } + + private fun TypeSpec.Builder.configureColumnProperties(table: TableMetadata): TypeSpec.Builder { + for (column in table.columns) { + val columnType = Column::class.asClassName().parameterizedBy(column.getKotlinType()) + val propertySpec = PropertySpec.builder(column.columnPropertyName, columnType) + .addKdoc("Column %L. %L", column.name, column.entityProperty.docString?.trimIndent().orEmpty()) + .initializer(buildCodeBlock { + add(column.getRegisteringCodeBlock()) + + if (column.isPrimaryKey) { + add(".primaryKey()") + } + + if (table.entityClass.classKind == ClassKind.INTERFACE) { + if (column.isReference) { + val pkg = column.referenceTable!!.entityClass.packageName.asString() + val name = column.referenceTable!!.tableClassName + val propName = column.entityProperty.simpleName.asString() + add(".references(%T)·{·it.%N·}", ClassName(pkg, name), propName) + } else { + add(".bindTo·{·it.%N·}", column.entityProperty.simpleName.asString()) + } + } + }) + .build() + + addProperty(propertySpec) + } + + return this + } + + private fun TypeSpec.Builder.configureDoCreateEntityFunction( + table: TableMetadata, options: Map + ): TypeSpec.Builder { + if (table.entityClass.classKind == ClassKind.INTERFACE) { + return this + } + + val func = FunSpec.builder("doCreateEntity") + .addKdoc("Create an entity object from the specific row of query results.") + .addModifiers(KModifier.OVERRIDE) + .addParameter("row", QueryRowSet::class.asTypeName()) + .addParameter("withReferences", Boolean::class.asTypeName()) + .returns(table.entityClass.toClassName()) + .addCode(buildCodeBlock { buildDoCreateEntityFunctionBody(table, options) }) + .build() + + addFunction(func) + return this + } + + private fun CodeBlock.Builder.buildDoCreateEntityFunctionBody(table: TableMetadata, options: Map) { + val constructorParams = table.entityClass.primaryConstructor!!.parameters.associateBy { it.name!!.asString() } + + val hasDefaultValues = table.columns + .mapNotNull { constructorParams[it.entityProperty.simpleName.asString()] } + .any { it.hasDefault } + + if (hasDefaultValues && options["ktorm.allowReflection"] == "true") { + createEntityByReflection(table, constructorParams) + } else { + createEntityByConstructor(table, constructorParams) + } + + for (column in table.columns) { + val propName = column.entityProperty.simpleName.asString() + if (propName in constructorParams) { + continue + } + + if (column.entityProperty._type.isMarkedNullable) { + addStatement("entity.%N·=·row[this.%N]", propName, column.columnPropertyName) + } else { + addStatement("entity.%N·=·row[this.%N]!!", propName, column.columnPropertyName) + } + } + + if (table.columns.any { it.entityProperty.simpleName.asString() !in constructorParams }) { + addStatement("return·entity") + } + } + + private fun CodeBlock.Builder.createEntityByReflection( + table: TableMetadata, constructorParams: Map + ) { + addStatement( + "val constructor = %T::class.%M!!", + table.entityClass.toClassName(), + MemberName("kotlin.reflect.full", "primaryConstructor", true) + ) + + add("«val args = mapOf(") + + for (column in table.columns) { + val propName = column.entityProperty.simpleName.asString() + if (propName in constructorParams) { + add( + "constructor.%M(%S)!! to row[this.%N],", + MemberName("kotlin.reflect.full", "findParameterByName", true), + propName, + column.columnPropertyName + ) + } + } + + add(")\n»") + addStatement("// Filter optional arguments out to make default values work.") + + if (table.columns.all { it.entityProperty.simpleName.asString() in constructorParams }) { + addStatement("return constructor.callBy(args.filterNot { (k, v) -> k.isOptional && v == null })") + } else { + addStatement("val entity = constructor.callBy(args.filterNot { (k, v) -> k.isOptional && v == null })") + } + } + + private fun CodeBlock.Builder.createEntityByConstructor( + table: TableMetadata, constructorParams: Map + ) { + if (table.columns.all { it.entityProperty.simpleName.asString() in constructorParams }) { + add("«return·%T(", table.entityClass.toClassName()) + } else { + add("«val·entity·=·%T(", table.entityClass.toClassName()) + } + + for (column in table.columns) { + val parameter = constructorParams[column.entityProperty.simpleName.asString()] ?: continue + if (parameter._type.isMarkedNullable) { + add("%N·=·row[this.%N],", parameter.name!!.asString(), column.columnPropertyName) + } else { + add("%N·=·row[this.%N]!!,", parameter.name!!.asString(), column.columnPropertyName) + } + } + + add(")\n»") + } + + private fun TypeSpec.Builder.configureAliasedFunction(table: TableMetadata): TypeSpec.Builder { + val kdoc = "" + + "Return a new-created table object with all properties (including the table name and columns " + + "and so on) being copied from this table, but applying a new alias given by the parameter." + + val func = FunSpec.builder("aliased") + .addKdoc(kdoc) + .addModifiers(KModifier.OVERRIDE) + .addParameter("alias", typeNameOf()) + .returns(ClassName(table.entityClass.packageName.asString(), table.tableClassName)) + .addCode("return %L(alias)", table.tableClassName) + .build() + + addFunction(func) + return this + } + + private fun TypeSpec.Builder.configureCompanionObject(table: TableMetadata): TypeSpec.Builder { + val companion = TypeSpec.companionObjectBuilder(null) + .addKdoc("The default table object of %L.", table.name) + .superclass(ClassName(table.entityClass.packageName.asString(), table.tableClassName)) + .addSuperclassConstructorParameter(CodeBlock.of("alias·=·%S", table.alias)) + .build() + + addType(companion) + return this + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/UpdateFunctionGenerator.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/UpdateFunctionGenerator.kt new file mode 100644 index 000000000..508d8a8a2 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/generator/UpdateFunctionGenerator.kt @@ -0,0 +1,121 @@ +/* + * 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.compiler.generator + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import org.ktorm.dsl.AliasRemover +import org.ktorm.entity.EntitySequence +import org.ktorm.expression.ColumnAssignmentExpression +import org.ktorm.expression.UpdateExpression +import org.ktorm.ksp.compiler.util._type +import org.ktorm.ksp.spi.TableMetadata + +@OptIn(KotlinPoetKspPreview::class) +internal object UpdateFunctionGenerator { + + fun generate(table: TableMetadata): FunSpec { + val kdoc = """ + Update the given entity to the database. + + @param entity the entity to be updated. + @param isDynamic whether only non-null columns should be updated. + @return the affected record number. + """.trimIndent() + + val entityClass = table.entityClass.toClassName() + val tableClass = ClassName(table.entityClass.packageName.asString(), table.tableClassName) + + return FunSpec.builder("update") + .addKdoc(kdoc) + .receiver(EntitySequence::class.asClassName().parameterizedBy(entityClass, tableClass)) + .addParameter("entity", entityClass) + .addParameter(ParameterSpec.builder("isDynamic", typeNameOf()).defaultValue("false").build()) + .returns(Int::class.asClassName()) + .addCode(AddFunctionGenerator.checkForDml()) + .addCode(AddFunctionGenerator.addValFun(table, useGeneratedKey = false)) + .addCode(addAssignments(table)) + .addCode(createExpression(table)) + .addStatement("return database.executeUpdate(expression)") + .build() + } + + private fun addAssignments(table: TableMetadata): CodeBlock { + return buildCodeBlock { + addStatement("val assignments = ArrayList<%T<*>>()", ColumnAssignmentExpression::class.asClassName()) + + for (column in table.columns) { + if (column.isPrimaryKey) { + continue + } + + addStatement( + "assignments.addVal(sourceTable.%N, entity.%N)", + column.columnPropertyName, + column.entityProperty.simpleName.asString() + ) + } + + beginControlFlow("if (assignments.isEmpty())") + addStatement("return 0") + endControlFlow() + + add("\n") + } + } + + private fun createExpression(table: TableMetadata): CodeBlock { + return buildCodeBlock { + addStatement( + "val visitor = database.dialect.createExpressionVisitor(%T)", + AliasRemover::class.asClassName() + ) + + add("«val conditions = ") + + val primaryKeys = table.columns.filter { it.isPrimaryKey } + for ((i, column) in primaryKeys.withIndex()) { + val condition: String + if (column.entityProperty._type.isMarkedNullable) { + condition = "(sourceTable.%N·%M·entity.%N!!)" + } else { + condition = "(sourceTable.%N·%M·entity.%N)" + } + + add( + condition, + column.columnPropertyName, + MemberName("org.ktorm.dsl", "eq", true), + column.entityProperty.simpleName.asString() + ) + + if (i < primaryKeys.lastIndex) { + add("·%M·", MemberName("org.ktorm.dsl", "and", true)) + } + } + + add("\n»") + + addStatement( + "val expression = visitor.visit(%T(sourceTable.asExpression(), assignments, conditions))", + UpdateExpression::class.asClassName() + ) + } + } +} 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 new file mode 100644 index 000000000..55b94489b --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/parser/MetadataParser.kt @@ -0,0 +1,299 @@ +/* + * 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.compiler.parser + +import com.google.devtools.ksp.* +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 org.ktorm.entity.Entity +import org.ktorm.ksp.annotation.* +import org.ktorm.ksp.compiler.util.* +import org.ktorm.ksp.spi.CodingNamingStrategy +import org.ktorm.ksp.spi.ColumnMetadata +import org.ktorm.ksp.spi.DatabaseNamingStrategy +import org.ktorm.ksp.spi.TableMetadata +import org.ktorm.schema.TypeReference +import java.lang.reflect.InvocationTargetException +import java.util.* +import kotlin.reflect.jvm.jvmName + +@OptIn(KspExperimental::class) +internal class MetadataParser(resolver: Resolver, environment: SymbolProcessorEnvironment) { + private val _resolver = resolver + private val _options = environment.options + private val _logger = environment.logger + private val _databaseNamingStrategy = loadDatabaseNamingStrategy() + private val _codingNamingStrategy = loadCodingNamingStrategy() + private val _tablesCache = HashMap() + + @Suppress("SwallowedException") + private fun loadDatabaseNamingStrategy(): DatabaseNamingStrategy { + val name = _options["ktorm.dbNamingStrategy"] ?: "lower-snake-case" + if (name == "lower-snake-case") { + return LowerSnakeCaseDatabaseNamingStrategy + } + if (name == "upper-snake-case") { + return UpperSnakeCaseDatabaseNamingStrategy + } + + try { + val cls = Class.forName(name) + return (cls.kotlin.objectInstance ?: cls.getDeclaredConstructor().newInstance()) as DatabaseNamingStrategy + } catch (e: InvocationTargetException) { + throw e.targetException + } + } + + @Suppress("SwallowedException") + private fun loadCodingNamingStrategy(): CodingNamingStrategy { + val name = _options["ktorm.codingNamingStrategy"] ?: return DefaultCodingNamingStrategy + + try { + val cls = Class.forName(name) + return (cls.kotlin.objectInstance ?: cls.getDeclaredConstructor().newInstance()) as CodingNamingStrategy + } catch (e: InvocationTargetException) { + throw e.targetException + } + } + + fun parseTableMetadata(cls: KSClassDeclaration): TableMetadata { + val className = cls.qualifiedName!!.asString() + val r = _tablesCache[className] + if (r != null) { + return r + } + + if (cls.classKind != CLASS && cls.classKind != INTERFACE) { + throw IllegalStateException("$className should be a class or interface but actually ${cls.classKind}.") + } + + if (cls.classKind == INTERFACE && !cls.isSubclassOf>()) { + throw IllegalStateException("$className should extend from org.ktorm.entity.Entity.") + } + + if (cls.classKind == CLASS && cls.isAbstract()) { + throw IllegalStateException("$className cannot be an abstract class.") + } + + _logger.info("[ktorm-ksp-compiler] parse table metadata from entity: $className") + val table = cls.getAnnotationsByType(Table::class).first() + val tableMetadata = TableMetadata( + entityClass = cls, + name = table.name.ifEmpty { _databaseNamingStrategy.getTableName(cls) }, + alias = table.alias.takeIf { it.isNotEmpty() }, + catalog = table.catalog.ifEmpty { _options["ktorm.catalog"] }?.takeIf { it.isNotEmpty() }, + 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() + ) + + val columns = tableMetadata.columns as MutableList + for (property in cls.getProperties(tableMetadata.ignoreProperties)) { + if (property.isAnnotationPresent(References::class)) { + columns += parseRefColumnMetadata(property, tableMetadata) + } else { + columns += parseColumnMetadata(property, tableMetadata) + } + } + + _tablesCache[className] = tableMetadata + return tableMetadata + } + + private fun KSClassDeclaration.getProperties(ignoreProperties: Set): Sequence { + val constructorParams = HashSet() + if (classKind == CLASS) { + primaryConstructor?.parameters?.mapTo(constructorParams) { it.name!!.asString() } + } + + return this.getAllProperties() + .filterNot { it.simpleName.asString() in ignoreProperties } + .filterNot { it.isAnnotationPresent(Ignore::class) } + .filterNot { classKind == CLASS && !it.hasBackingField } + .filterNot { classKind == INTERFACE && !it.isAbstract() } + .filterNot { classKind == INTERFACE && it.simpleName.asString() in setOf("entityClass", "properties") } + .sortedByDescending { it.simpleName.asString() in constructorParams } + } + + private fun parseColumnMetadata(property: KSPropertyDeclaration, table: TableMetadata): ColumnMetadata { + // @Column annotation is optional. + val column = property.getAnnotationsByType(Column::class).firstOrNull() + + var name = column?.name + if (name.isNullOrEmpty()) { + name = _databaseNamingStrategy.getColumnName(table.entityClass, property) + } + + var propertyName = column?.propertyName + if (propertyName.isNullOrEmpty()) { + propertyName = _codingNamingStrategy.getColumnPropertyName(table.entityClass, property) + } + + return ColumnMetadata( + entityProperty = property, + table = table, + name = name, + isPrimaryKey = property.isAnnotationPresent(PrimaryKey::class), + sqlType = parseColumnSqlType(property), + isReference = false, + referenceTable = null, + columnPropertyName = propertyName, + refTablePropertyName = null + ) + } + + private fun parseColumnSqlType(property: KSPropertyDeclaration): KSType { + val propName = property.qualifiedName?.asString() + var sqlType = property.annotations + .filter { it._annotationType.declaration.qualifiedName?.asString() == Column::class.jvmName } + .flatMap { it.arguments } + .filter { it.name?.asString() == Column::sqlType.name } + .map { it.value as KSType? } + .singleOrNull() + + if (sqlType?.declaration?.qualifiedName?.asString() == Nothing::class.jvmName) { + sqlType = null + } + + if (sqlType == null) { + sqlType = property.getSqlType(_resolver) + } + + if (sqlType == null) { + val msg = "Parse sqlType error for property $propName: cannot infer sqlType, please specify manually." + throw IllegalArgumentException(msg) + } + + val declaration = sqlType.declaration as KSClassDeclaration + if (declaration.classKind != OBJECT) { + if (declaration.isAbstract()) { + val msg = "Parse sqlType error for property $propName: the sqlType class cannot be abstract." + throw IllegalArgumentException(msg) + } + + val hasConstructor = declaration.getConstructors() + .filter { it.parameters.size == 1 } + .filter { it.parameters[0]._type.declaration.qualifiedName?.asString() == TypeReference::class.jvmName } + .any() + + 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." + throw IllegalArgumentException(msg) + } + } + + return sqlType + } + + private fun parseRefColumnMetadata(property: KSPropertyDeclaration, table: TableMetadata): ColumnMetadata { + val propName = property.qualifiedName?.asString() + if (property.isAnnotationPresent(Column::class)) { + val msg = "Parse ref column error for property $propName: @Column and @References cannot be used together." + throw IllegalStateException(msg) + } + + if (table.entityClass.classKind != INTERFACE) { + val msg = + "Parse ref column error for property $propName: @References only allowed in interface-based entities." + throw IllegalStateException(msg) + } + + val refEntityClass = property._type.declaration as KSClassDeclaration + table.checkCircularRef(refEntityClass) + + if (refEntityClass.classKind != INTERFACE) { + val msg = "Parse ref column error for property $propName: the referenced entity class must be an interface." + throw IllegalStateException(msg) + } + + if (!refEntityClass.isAnnotationPresent(Table::class)) { + val msg = + "Parse ref column error for property $propName: the referenced entity must be annotated with @Table." + throw IllegalStateException(msg) + } + + val refTable = parseTableMetadata(refEntityClass) + val primaryKeys = refTable.columns.filter { it.isPrimaryKey } + if (primaryKeys.isEmpty()) { + val msg = "Parse ref column error for property $propName: the referenced table doesn't have a primary key." + throw IllegalStateException(msg) + } + + if (primaryKeys.size > 1) { + val msg = + "Parse ref column error for property $propName: the referenced table cannot have compound primary keys." + throw IllegalStateException(msg) + } + + val reference = property.getAnnotationsByType(References::class).first() + var name = reference.name + if (name.isEmpty()) { + name = _databaseNamingStrategy.getRefColumnName(table.entityClass, property, refTable) + } + + var propertyName = reference.propertyName + if (propertyName.isEmpty()) { + propertyName = _codingNamingStrategy.getRefColumnPropertyName(table.entityClass, property, refTable) + } + + var refTablePropertyName = reference.refTablePropertyName + if (refTablePropertyName.isEmpty()) { + refTablePropertyName = _codingNamingStrategy.getRefTablePropertyName(table.entityClass, property, refTable) + } + + return ColumnMetadata( + entityProperty = property, + table = table, + name = name, + isPrimaryKey = property.isAnnotationPresent(PrimaryKey::class), + sqlType = primaryKeys[0].sqlType, + isReference = true, + referenceTable = refTable, + columnPropertyName = propertyName, + refTablePropertyName = refTablePropertyName + ) + } + + private fun TableMetadata.checkCircularRef(ref: KSClassDeclaration, stack: LinkedList = LinkedList()) { + val className = this.entityClass.qualifiedName?.asString() + val refClassName = ref.qualifiedName?.asString() + + stack.push(refClassName) + + if (className == refClassName) { + val route = stack.asReversed().joinToString(separator = " --> ") + val msg = "Circular reference is not allowed, current table: $className, reference route: $route." + throw IllegalStateException(msg) + } + + val refTable = ref.getAnnotationsByType(Table::class).firstOrNull() + for (property in ref.getProperties(refTable?.ignoreProperties?.toSet() ?: emptySet())) { + if (property.isAnnotationPresent(References::class)) { + val propType = property._type.declaration as KSClassDeclaration + checkCircularRef(propType, stack) + } + } + + stack.pop() + } +} 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 new file mode 100644 index 000000000..fc96866aa --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/KspExtensions.kt @@ -0,0 +1,117 @@ +/* + * 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. + */ + +@file:OptIn(KspExperimental::class) + +package org.ktorm.ksp.compiler.util + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.visitor.KSValidateVisitor + +/** + * Return the resolved [KSType] of this property. + */ +@Suppress("ObjectPropertyName", "TopLevelPropertyNaming") +internal val KSPropertyDeclaration._type: KSType get() = type.resolve() + +/** + * Return the resolved [KSType] of this parameter. + */ +@Suppress("ObjectPropertyName", "TopLevelPropertyNaming") +internal val KSValueParameter._type: KSType get() = type.resolve() + +/** + * Return the resolved [KSType] of this annotation. + */ +@Suppress("ObjectPropertyName", "TopLevelPropertyNaming") +internal val KSAnnotation._annotationType: KSType get() = annotationType.resolve() + +/** + * Check if this type is an inline class. + */ +internal fun KSType.isInline(): Boolean { + val declaration = declaration as KSClassDeclaration + return declaration.isAnnotationPresent(JvmInline::class) && declaration.modifiers.contains(Modifier.VALUE) +} + +/** + * Check if this class is a subclass of [T]. + */ +internal inline fun KSClassDeclaration.isSubclassOf(): Boolean { + return findSuperTypeReference(T::class.qualifiedName!!) != null +} + +/** + * Find the specific super type reference for this class. + */ +internal fun KSClassDeclaration.findSuperTypeReference(name: String): KSTypeReference? { + for (superType in this.superTypes) { + val ksType = superType.resolve() + + if (ksType.declaration.qualifiedName?.asString() == name) { + return superType + } + + val result = (ksType.declaration as KSClassDeclaration).findSuperTypeReference(name) + if (result != null) { + return result + } + } + + return null +} + +/** + * Check if the given symbol is valid. + */ +internal fun KSNode.isValid(): Boolean { + // Custom visitor to avoid stack overflow, see https://github.com/google/ksp/issues/1114 + val visitor = object : KSValidateVisitor({ _, _ -> true }) { + private val stack = LinkedHashSet() + + private fun validateType(type: KSType): Boolean { + if (!stack.add(type)) { + // Skip if the type already in the stack, avoid infinite recursion. + return true + } + + try { + return !type.isError && !type.arguments.any { it.type?.accept(this, null) == false } + } finally { + stack.remove(type) + } + } + + override fun visitTypeReference(typeReference: KSTypeReference, data: KSNode?): Boolean { + return validateType(typeReference.resolve()) + } + + override fun visitValueArgument(valueArgument: KSValueArgument, data: KSNode?): Boolean { + fun visitValue(value: Any?): Boolean = when (value) { + is KSType -> this.validateType(value) + is KSAnnotation -> this.visitAnnotation(value, data) + is List<*> -> value.all { visitValue(it) } + else -> true + } + + return visitValue(valueArgument.value) + } + } + + return this.accept(visitor, null) +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/Namings.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/Namings.kt new file mode 100644 index 000000000..398a822e0 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/Namings.kt @@ -0,0 +1,102 @@ +/* + * 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.compiler.util + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import org.atteo.evo.inflector.English +import org.ktorm.ksp.spi.CodingNamingStrategy +import org.ktorm.ksp.spi.DatabaseNamingStrategy +import org.ktorm.ksp.spi.TableMetadata + +internal object LowerSnakeCaseDatabaseNamingStrategy : DatabaseNamingStrategy { + + override fun getTableName(c: KSClassDeclaration): String { + return CamelCase.toLowerSnakeCase(c.simpleName.asString()) + } + + override fun getColumnName(c: KSClassDeclaration, prop: KSPropertyDeclaration): String { + return CamelCase.toLowerSnakeCase(prop.simpleName.asString()) + } + + override fun getRefColumnName(c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata): String { + val pk = ref.columns.single { it.isPrimaryKey } + return CamelCase.toLowerSnakeCase(prop.simpleName.asString()) + "_" + pk.name + } +} + +internal object UpperSnakeCaseDatabaseNamingStrategy : DatabaseNamingStrategy { + + override fun getTableName(c: KSClassDeclaration): String { + return CamelCase.toUpperSnakeCase(c.simpleName.asString()) + } + + override fun getColumnName(c: KSClassDeclaration, prop: KSPropertyDeclaration): String { + return CamelCase.toUpperSnakeCase(prop.simpleName.asString()) + } + + override fun getRefColumnName(c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata): String { + val pk = ref.columns.single { it.isPrimaryKey } + return CamelCase.toUpperSnakeCase(prop.simpleName.asString()) + "_" + pk.name + } +} + +internal object DefaultCodingNamingStrategy : CodingNamingStrategy { + + override fun getTableClassName(c: KSClassDeclaration): String { + return English.plural(c.simpleName.asString()) + } + + override fun getEntitySequenceName(c: KSClassDeclaration): String { + return CamelCase.toFirstLowerCamelCase(English.plural(c.simpleName.asString())) + } + + override fun getColumnPropertyName(c: KSClassDeclaration, prop: KSPropertyDeclaration): String { + return prop.simpleName.asString() + } + + override fun getRefColumnPropertyName( + c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata + ): String { + val pk = ref.columns.single { it.isPrimaryKey } + return prop.simpleName.asString() + pk.columnPropertyName.replaceFirstChar { it.uppercase() } + } + + override fun getRefTablePropertyName( + c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata + ): String { + return prop.simpleName.asString() + } +} + +internal object CamelCase { + // Matches boundaries between words, for example (abc|Def), (ABC|Def) + private val boundaries = listOf(Regex("([a-z])([A-Z])"), Regex("([A-Z])([A-Z][a-z])")) + + fun toLowerSnakeCase(name: String): String { + return boundaries.fold(name) { s, regex -> s.replace(regex, "$1_$2") }.lowercase() + } + + fun toUpperSnakeCase(name: String): String { + return boundaries.fold(name) { s, regex -> s.replace(regex, "$1_$2") }.uppercase() + } + + fun toFirstLowerCamelCase(name: String): String { + val i = boundaries.mapNotNull { regex -> regex.find(name) }.minOfOrNull { it.range.first } ?: 0 + return name.substring(0, i + 1).lowercase() + name.substring(i + 1) + } +} diff --git a/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/SqlTypeMappings.kt b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/SqlTypeMappings.kt new file mode 100644 index 000000000..c99976941 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/kotlin/org/ktorm/ksp/compiler/util/SqlTypeMappings.kt @@ -0,0 +1,128 @@ +/* + * 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. + */ + +@file:OptIn(KotlinPoetKspPreview::class) + +package org.ktorm.ksp.compiler.util + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import org.ktorm.ksp.spi.ColumnMetadata +import org.ktorm.schema.* + +internal fun KSPropertyDeclaration.getSqlType(resolver: Resolver): KSType? { + val declaration = _type.declaration as KSClassDeclaration + if (declaration.classKind == ClassKind.ENUM_CLASS) { + return resolver.getClassDeclarationByName>()?.asType(emptyList()) + } + + val sqlType = when (declaration.qualifiedName?.asString()) { + "kotlin.Boolean" -> BooleanSqlType::class + "kotlin.Int" -> IntSqlType::class + "kotlin.Short" -> ShortSqlType::class + "kotlin.Long" -> LongSqlType::class + "kotlin.Float" -> FloatSqlType::class + "kotlin.Double" -> DoubleSqlType::class + "kotlin.String" -> VarcharSqlType::class + "kotlin.ByteArray" -> BytesSqlType::class + "java.math.BigDecimal" -> DecimalSqlType::class + "java.sql.Timestamp" -> TimestampSqlType::class + "java.sql.Date" -> DateSqlType::class + "java.sql.Time" -> TimeSqlType::class + "java.time.Instant" -> InstantSqlType::class + "java.time.LocalDateTime" -> LocalDateTimeSqlType::class + "java.time.LocalDate" -> LocalDateSqlType::class + "java.time.LocalTime" -> LocalTimeSqlType::class + "java.time.MonthDay" -> MonthDaySqlType::class + "java.time.YearMonth" -> YearMonthSqlType::class + "java.time.Year" -> YearSqlType::class + "java.util.UUID" -> UuidSqlType::class + else -> null + } + + return sqlType?.qualifiedName?.let { resolver.getClassDeclarationByName(it)?.asType(emptyList()) } +} + +internal fun ColumnMetadata.getRegisteringCodeBlock(): CodeBlock { + val sqlTypeName = sqlType.declaration.qualifiedName?.asString() + val registerFun = when (sqlTypeName) { + "org.ktorm.schema.BooleanSqlType" -> MemberName("org.ktorm.schema", "boolean", true) + "org.ktorm.schema.IntSqlType" -> MemberName("org.ktorm.schema", "int", true) + "org.ktorm.schema.ShortSqlType" -> MemberName("org.ktorm.schema", "short", true) + "org.ktorm.schema.LongSqlType" -> MemberName("org.ktorm.schema", "long", true) + "org.ktorm.schema.FloatSqlType" -> MemberName("org.ktorm.schema", "float", true) + "org.ktorm.schema.DoubleSqlType" -> MemberName("org.ktorm.schema", "double", true) + "org.ktorm.schema.DecimalSqlType" -> MemberName("org.ktorm.schema", "decimal", true) + "org.ktorm.schema.VarcharSqlType" -> MemberName("org.ktorm.schema", "varchar", true) + "org.ktorm.schema.TextSqlType" -> MemberName("org.ktorm.schema", "text", true) + "org.ktorm.schema.BlobSqlType" -> MemberName("org.ktorm.schema", "blob", true) + "org.ktorm.schema.BytesSqlType" -> MemberName("org.ktorm.schema", "bytes", true) + "org.ktorm.schema.TimestampSqlType" -> MemberName("org.ktorm.schema", "jdbcTimestamp", true) + "org.ktorm.schema.DateSqlType" -> MemberName("org.ktorm.schema", "jdbcDate", true) + "org.ktorm.schema.TimeSqlType" -> MemberName("org.ktorm.schema", "jdbcTime", true) + "org.ktorm.schema.InstantSqlType" -> MemberName("org.ktorm.schema", "timestamp", true) + "org.ktorm.schema.LocalDateTimeSqlType" -> MemberName("org.ktorm.schema", "datetime", true) + "org.ktorm.schema.LocalDateSqlType" -> MemberName("org.ktorm.schema", "date", true) + "org.ktorm.schema.LocalTimeSqlType" -> MemberName("org.ktorm.schema", "time", true) + "org.ktorm.schema.MonthDaySqlType" -> MemberName("org.ktorm.schema", "monthDay", true) + "org.ktorm.schema.YearMonthSqlType" -> MemberName("org.ktorm.schema", "yearMonth", true) + "org.ktorm.schema.YearSqlType" -> MemberName("org.ktorm.schema", "year", true) + "org.ktorm.schema.UuidSqlType" -> MemberName("org.ktorm.schema", "uuid", true) + else -> null + } + + if (registerFun != null) { + return CodeBlock.of("%M(%S)", registerFun, name) + } + + if (sqlTypeName == "org.ktorm.schema.EnumSqlType") { + return CodeBlock.of("%M<%T>(%S)", MemberName("org.ktorm.schema", "enum", true), getKotlinType(), name) + } + + if (sqlTypeName == "org.ktorm.jackson.JsonSqlType") { + return CodeBlock.of("%M<%T>(%S)", MemberName("org.ktorm.jackson", "json", true), getKotlinType(), name) + } + + val declaration = sqlType.declaration as KSClassDeclaration + if (declaration.classKind == ClassKind.OBJECT) { + return CodeBlock.of("registerColumn(%S,·%T)", name, declaration.toClassName()) + } else { + return CodeBlock.of("registerColumn(%S,·%T(%M<%T>()))", + name, + declaration.toClassName(), + MemberName("org.ktorm.schema", "typeRef"), + getKotlinType() + ) + } +} + +internal fun ColumnMetadata.getKotlinType(): TypeName { + if (isReference) { + return referenceTable!!.columns.single { it.isPrimaryKey }.getKotlinType() + } else { + return entityProperty._type.makeNotNullable().toTypeName() + } +} diff --git a/ktorm-ksp-compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/ktorm-ksp-compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 000000000..091bb5f70 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +org.ktorm.ksp.compiler.KtormProcessorProvider diff --git a/ktorm-ksp-compiler/src/main/resources/ktorm-ksp-compiler/.editorconfig b/ktorm-ksp-compiler/src/main/resources/ktorm-ksp-compiler/.editorconfig new file mode 100644 index 000000000..9e72d1b64 --- /dev/null +++ b/ktorm-ksp-compiler/src/main/resources/ktorm-ksp-compiler/.editorconfig @@ -0,0 +1,11 @@ +# ktlint config used to format the generated code. +[*.{kt,kts}] +ktlint_code_style = ktlint_official +indent_size = 4 +indent_style = space +max_line_length = 120 +insert_final_newline = true +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 +ktlint_function_signature_body_expression_wrapping = multiline +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false 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 new file mode 100644 index 000000000..5b882e889 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/BaseKspTest.kt @@ -0,0 +1,122 @@ +package org.ktorm.ksp.compiler + +import com.tschuchort.compiletesting.* +import org.intellij.lang.annotations.Language +import org.junit.After +import org.junit.Before +import org.ktorm.database.Database +import org.ktorm.database.use +import java.lang.reflect.InvocationTargetException + +abstract class BaseKspTest { + lateinit var database: Database + + @Before + fun init() { + database = Database.connect("jdbc:h2:mem:ktorm;DB_CLOSE_DELAY=-1", alwaysQuoteIdentifiers = true) + execSqlScript("init-ksp-data.sql") + } + + @After + fun destroy() { + execSqlScript("drop-ksp-data.sql") + } + + private fun execSqlScript(filename: String) { + database.useConnection { conn -> + conn.createStatement().use { statement -> + javaClass.classLoader + ?.getResourceAsStream(filename) + ?.bufferedReader() + ?.use { reader -> + for (sql in reader.readText().split(';')) { + if (sql.any { it.isLetterOrDigit() }) { + statement.executeUpdate(sql) + } + } + } + } + } + } + + protected fun kspFailing(message: String, @Language("kotlin") code: String, vararg options: Pair) { + val result = compile(code, mapOf(*options)) + assert(result.exitCode == KotlinCompilation.ExitCode.COMPILATION_ERROR) + assert(result.messages.contains("e: Error occurred in KSP, check log for detail")) + assert(result.messages.contains(message)) + } + + protected fun runKotlin(@Language("kotlin") code: String, vararg options: Pair) { + val result = compile(code, mapOf(*options)) + assert(result.exitCode == KotlinCompilation.ExitCode.OK) + + try { + val cls = result.classLoader.loadClass("SourceKt") + cls.getMethod("setDatabase", Database::class.java).invoke(null, database) + cls.getMethod("run").invoke(null) + } catch (e: InvocationTargetException) { + throw e.targetException + } + } + + private fun compile(@Language("kotlin") code: String, options: Map): KotlinCompilation.Result { + @Language("kotlin") + val header = """ + import java.math.* + import java.sql.* + import java.time.* + import java.util.* + import kotlin.reflect.* + import kotlin.reflect.jvm.* + import org.ktorm.database.* + import org.ktorm.dsl.* + import org.ktorm.entity.* + import org.ktorm.ksp.annotation.* + + lateinit var database: Database + + + """.trimIndent() + + val source = header + code + printFile(source, "Source.kt") + + val compilation = createCompilation(SourceFile.kotlin("Source.kt", source), options) + val result = compilation.compile() + + for (file in compilation.kspSourcesDir.walk()) { + if (file.isFile) { + printFile(file.readText(), "Generated file: ${file.absolutePath}") + } + } + + return result + } + + private fun createCompilation(source: SourceFile, options: Map): KotlinCompilation { + return KotlinCompilation().apply { + sources = listOf(source) + verbose = false + messageOutputStream = System.out + inheritClassPath = true + allWarningsAsErrors = true + symbolProcessorProviders = listOf(KtormProcessorProvider()) + kspIncremental = true + kspWithCompilation = true + kspArgs += options + } + } + + private fun printFile(text: String, title: String) { + val lines = text.lines() + val gutterSize = lines.size.toString().count() + + println("${"#".repeat(gutterSize + 2)}-----------------------------------------") + println("${"#".repeat(gutterSize + 2)} $title") + println("${"#".repeat(gutterSize + 2)}-----------------------------------------") + + for ((i, line) in lines.withIndex()) { + println(String.format("#%${gutterSize}d| %s", i + 1, line)) + } + } +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/AddFunctionGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/AddFunctionGeneratorTest.kt new file mode 100644 index 000000000..aaa3613e9 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/AddFunctionGeneratorTest.kt @@ -0,0 +1,95 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class AddFunctionGeneratorTest : BaseKspTest() { + + @Test + fun testGenerateKey() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int, + ) + + fun run() { + val user = User(0, "test", 100) + database.users.add(user, useGeneratedKey = true) + assert(user.id == 4) + + val users = database.users.toList() + assert(users.size == 4) + assert(users[0] == User(id = 1, username = "jack", age = 20)) + assert(users[1] == User(id = 2, username = "lucy", age = 22)) + assert(users[2] == User(id = 3, username = "mike", age = 22)) + assert(users[3] == User(id = 4, username = "test", age = 100)) + } + """.trimIndent()) + + @Test + fun testNoGenerateKey() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int, + ) + + fun run() { + database.users.add(User(99, "test", 100)) + + val users = database.users.toList() + assert(users.size == 4) + assert(users[0] == User(id = 1, username = "jack", age = 20)) + assert(users[1] == User(id = 2, username = "lucy", age = 22)) + assert(users[2] == User(id = 3, username = "mike", age = 22)) + assert(users[3] == User(id = 99, username = "test", age = 100)) + } + """.trimIndent()) + + @Test + fun testNoGenerateKey1() = runKotlin(""" + @Table + data class User( + @PrimaryKey + val id: Int, + val username: String, + val age: Int, + ) + + fun run() { + database.users.add(User(99, "test", 100)) + + val users = database.users.toList() + assert(users.size == 4) + assert(users[0] == User(id = 1, username = "jack", age = 20)) + assert(users[1] == User(id = 2, username = "lucy", age = 22)) + assert(users[2] == User(id = 3, username = "mike", age = 22)) + assert(users[3] == User(id = 99, username = "test", age = 100)) + } + """.trimIndent()) + + @Test + fun testSequenceModified() = runKotlin(""" + @Table + data class User( + @PrimaryKey + val id: Int, + val username: String, + val age: Int, + ) + + fun run() { + try { + val users = database.users.filter { it.id eq 1 } + users.add(User(99, "lucy", 10)) + throw AssertionError("fail") + } catch (_: UnsupportedOperationException) { + } + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGeneratorTest.kt new file mode 100644 index 000000000..49167c089 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/ComponentFunctionGeneratorTest.kt @@ -0,0 +1,36 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class ComponentFunctionGeneratorTest : BaseKspTest() { + + @Test + fun `interface entity component function`() = runKotlin(""" + @Table + interface Employee: Entity { + @PrimaryKey + var id: Int? + var name: String + var job: String + @Ignore + var hireDate: LocalDate + } + + fun run() { + val today = LocalDate.now() + + val employee = Entity.create() + employee.id = 1 + employee.name = "name" + employee.job = "job" + employee.hireDate = today + + val (id, name, job, hireDate) = employee + assert(id == 1) + assert(name == "name") + assert(job == "job") + assert(hireDate == today) + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/CopyFunctionGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/CopyFunctionGeneratorTest.kt new file mode 100644 index 000000000..35e1327d3 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/CopyFunctionGeneratorTest.kt @@ -0,0 +1,37 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class CopyFunctionGeneratorTest : BaseKspTest() { + + @Test + fun `interface entity copy function`() = runKotlin(""" + @Table + interface Employee: Entity { + @PrimaryKey + var id: Int? + var name: String + var job: String + @Ignore + var hireDate: LocalDate + } + + fun run() { + val today = LocalDate.now() + + val jack = Employee(name = "jack", job = "programmer", hireDate = today) + val tom = jack.copy(name = "tom") + + assert(tom != jack) + assert(tom !== jack) + assert(tom.name == "tom") + assert(tom.job == "programmer") + assert(tom.hireDate == today) + + with(EntityExtensionsApi()) { + assert(!tom.hasColumnValue(Employees.id.binding!!)) + } + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/EntitySequencePropertyGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/EntitySequencePropertyGeneratorTest.kt new file mode 100644 index 000000000..32ac1d561 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/EntitySequencePropertyGeneratorTest.kt @@ -0,0 +1,61 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class EntitySequencePropertyGeneratorTest : BaseKspTest() { + + @Test + fun `sequenceOf function`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int + ) + + @Table + interface Employee: Entity { + @PrimaryKey + var id: Int + var name: String + var job: String + var hireDate: LocalDate + } + + fun run() { + val users = database.users.toList() + assert(users.size == 3) + assert(users[0] == User(id = 1, username = "jack", age = 20)) + assert(users[1] == User(id = 2, username = "lucy", age = 22)) + assert(users[2] == User(id = 3, username = "mike", age = 22)) + + val employees = database.employees.toList() + assert(employees.size == 4) + assert(employees[0] == Employee(id = 1, name = "vince", job = "engineer", hireDate = LocalDate.of(2018, 1, 1))) + assert(employees[1] == Employee(id = 2, name = "marry", job = "trainee", hireDate = LocalDate.of(2019, 1, 1))) + assert(employees[2] == Employee(id = 3, name = "tom", job = "director", hireDate = LocalDate.of(2018, 1, 1))) + assert(employees[3] == Employee(id = 4, name = "penny", job = "assistant", hireDate = LocalDate.of(2019, 1, 1))) + } + """.trimIndent()) + + @Test + fun `custom sequence name`() = runKotlin(""" + @Table(entitySequenceName = "aUsers") + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int + ) + + fun run() { + val users = database.aUsers.toList() + assert(users.size == 3) + assert(users[0] == User(id = 1, username = "jack", age = 20)) + assert(users[1] == User(id = 2, username = "lucy", age = 22)) + assert(users[2] == User(id = 3, username = "mike", age = 22)) + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/ExtCodeGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/ExtCodeGeneratorTest.kt new file mode 100644 index 000000000..1e750fc87 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/ExtCodeGeneratorTest.kt @@ -0,0 +1,43 @@ +package org.ktorm.ksp.compiler.generator + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.typeNameOf +import org.ktorm.ksp.spi.ExtCodeGenerator +import org.ktorm.ksp.spi.TableMetadata + +/** + * Created by vince at May 27, 2023. + */ +@OptIn(KotlinPoetKspPreview::class) +class ExtCodeGeneratorTest : ExtCodeGenerator { + + override fun generateTypes(table: TableMetadata, environment: SymbolProcessorEnvironment): List { + return listOf(TypeSpec.classBuilder("TestFor" + table.tableClassName).build()) + } + + override fun generateProperties(table: TableMetadata, environment: SymbolProcessorEnvironment): List { + return listOf( + PropertySpec.builder("pTest", typeNameOf()) + .addKdoc("This is a test property.") + .receiver(table.entityClass.toClassName()) + .getter(FunSpec.getterBuilder().addStatement("return 0").build()) + .build() + ) + } + + override fun generateFunctions(table: TableMetadata, environment: SymbolProcessorEnvironment): List { + return listOf( + FunSpec.builder("fTest") + .addKdoc("This is a test function. \n\n @since 3.6.0") + .receiver(table.entityClass.toClassName()) + .returns(typeNameOf()) + .addStatement("return 0") + .build() + ) + } +} \ No newline at end of file diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGeneratorTest.kt new file mode 100644 index 000000000..3a71cf7e1 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/PseudoConstructorFunctionGeneratorTest.kt @@ -0,0 +1,27 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class PseudoConstructorFunctionGeneratorTest : BaseKspTest() { + + @Test + fun `interface entity constructor function`() = runKotlin(""" + @Table + interface Employee: Entity { + @PrimaryKey + var id: Int? + var name: String + var job: String + @Ignore + var hireDate: LocalDate + } + + fun run() { + assert(Employee().toString() == "Employee()") + assert(Employee(id = null).toString() == "Employee(id=null)") + assert(Employee(id = null, name = "").toString() == "Employee(id=null, name=)") + assert(Employee(1, "vince", "engineer", LocalDate.of(2023, 1, 1)).toString() == "Employee(id=1, name=vince, job=engineer, hireDate=2023-01-01)") + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/RefsClassGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/RefsClassGeneratorTest.kt new file mode 100644 index 000000000..f3f331087 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/RefsClassGeneratorTest.kt @@ -0,0 +1,47 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +/** + * Created by vince at Jul 16, 2023. + */ +class RefsClassGeneratorTest : BaseKspTest() { + + @Test + fun testRefs() = runKotlin( + """ + @Table + interface Employee: Entity { + @PrimaryKey + var id: Int + var name: String + var job: String + var managerId: Int? + var hireDate: LocalDate + var salary: Long + @References + var department: Department + } + + @Table + interface Department: Entity { + @PrimaryKey + var id: Int + var name: String + var location: String + } + + fun run() { + val employees = database.employees + .filter { it.refs.department.name eq "tech" } + .filter { it.refs.department.location eq "Guangzhou" } + .sortedBy { it.id } + .toList() + + assert(employees.size == 2) + assert(employees[0].name == "vince") + assert(employees[1].name == "marry") + } + """.trimIndent()) +} \ No newline at end of file diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/TableClassGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/TableClassGeneratorTest.kt new file mode 100644 index 000000000..0d21efa85 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/TableClassGeneratorTest.kt @@ -0,0 +1,202 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class TableClassGeneratorTest : BaseKspTest() { + + @Test + fun `dataClass entity`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int? = null, + var username: String, + var age: Int = 0 + ) + + fun run() { + assert(Users.tableName == "user") + assert(Users.columns.map { it.name }.toSet() == setOf("id", "username", "age")) + } + """.trimIndent()) + + @Test + fun `data class keyword identifier`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var `class`: String, + var operator: String, + ) { + var `interface`: String = "" + var constructor: String = "" + } + + fun run() { + assert(Users.tableName == "user") + assert(Users.columns.map { it.name }.toSet() == setOf("id", "class", "operator", "interface", "constructor")) + } + """.trimIndent()) + + @Test + fun `table annotation`() = runKotlin(""" + @Table( + name = "t_user", + alias = "t_user_alias", + catalog = "catalog", + schema = "schema", + className = "UserTable", + entitySequenceName = "userTable", + ignoreProperties = ["age"] + ) + data class User( + @PrimaryKey + var id: Int, + var username: String, + ) { + var age: Int = 10 + } + + fun run() { + assert(UserTable.tableName == "t_user") + assert(UserTable.alias == "t_user_alias") + assert(UserTable.catalog == "catalog") + assert(UserTable.schema == "schema") + assert(UserTable.columns.map { it.name }.toSet() == setOf("id", "username")) + println(database.userTable) + } + """.trimIndent()) + + @Test + fun `data class constructor with default parameters column`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var phone: String? = "12345" + ) + + fun run() { + val user = database.users.first { it.id eq 1 } + assert(user.username == "jack") + assert(user.phone == null) + } + """.trimIndent()) + + @Test + fun `data class constructor with default parameters column allowing reflection`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var phone: String? = "12345" + ) + + fun run() { + val user = database.users.first { it.id eq 1 } + assert(user.username == "jack") + assert(user.phone == "12345") + } + """.trimIndent(), "ktorm.allowReflection" to "true") + + @Test + fun `ignore properties`() = runKotlin(""" + @Table(ignoreProperties = ["email"]) + data class User( + @PrimaryKey + var id: Int, + var age: Int, + @Ignore + var username: String = "" + ) { + var email: String = "" + } + + fun run() { + assert(Users.tableName == "user") + assert(Users.columns.map { it.name }.toSet() == setOf("id", "age")) + } + """.trimIndent()) + + @Test + fun `column has no backingField`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var age: Int + ) { + val username: String get() = "username" + } + + fun run() { + assert(Users.tableName == "user") + assert(Users.columns.map { it.name }.toSet() == setOf("id", "age")) + } + """.trimIndent()) + + @Test + fun `column reference`() = runKotlin(""" + @Table + interface User: Entity { + @PrimaryKey + var id: Int + var username: String + var age: Int + @References + var firstSchool: School + @References("second_school_identity") + var secondSchool: School + } + + @Table + interface School: Entity { + @PrimaryKey + var id: Int + var schoolName: String + } + + fun run() { + assert(Users.firstSchoolId.referenceTable is Schools) + assert(Users.firstSchoolId.name == "first_school_id") + assert(Users.secondSchoolId.referenceTable is Schools) + assert(Users.secondSchoolId.name == "second_school_identity") + } + """.trimIndent()) + + @Test + fun `interface entity`() = runKotlin(""" + @Table + interface User: Entity { + @PrimaryKey + var id: Int + var username: String + var age: Int + } + + fun run() { + assert(Users.tableName == "user") + assert(Users.columns.map { it.name }.toSet() == setOf("id", "username", "age")) + } + """.trimIndent()) + + @Test + fun `interface entity keyword identifier`() = runKotlin(""" + @Table + interface User: Entity { + @PrimaryKey + var id: Int + var `class`: String + var operator: String + } + + fun run() { + assert(Users.tableName == "user") + assert(Users.columns.map { it.name }.toSet() == setOf("id", "class", "operator")) + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/UpdateFunctionGeneratorTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/UpdateFunctionGeneratorTest.kt new file mode 100644 index 000000000..ca9fd2f27 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/generator/UpdateFunctionGeneratorTest.kt @@ -0,0 +1,49 @@ +package org.ktorm.ksp.compiler.generator + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class UpdateFunctionGeneratorTest : BaseKspTest() { + + @Test + fun `sequence update function`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int, + ) + + fun run() { + val user = database.users.first { it.id eq 1 } + assert(user.username == "jack") + + user.username = "tom" + database.users.update(user) + + val user0 = database.users.first { it.id eq 1 } + assert(user0.username == "tom") + } + """.trimIndent()) + + @Test + fun `modified entity sequence call update fun`() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int + ) + + fun run() { + try { + val users = database.users.filter { it.id eq 1 } + users.update(User(1, "lucy", 10)) + throw AssertionError("fail") + } catch (_: UnsupportedOperationException) { + } + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/CompoundPrimaryKeysTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/CompoundPrimaryKeysTest.kt new file mode 100644 index 000000000..22eac07e5 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/CompoundPrimaryKeysTest.kt @@ -0,0 +1,31 @@ +package org.ktorm.ksp.compiler.parser + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class CompoundPrimaryKeysTest : BaseKspTest() { + + @Test + fun `multi primary key`() = runKotlin(""" + @Table(name = "province") + data class Province( + @PrimaryKey + val country:String, + @PrimaryKey + val province:String, + var population:Int + ) + + fun run() { + database.provinces.add(Province("China", "Guangdong", 150000)) + assert(database.provinces.toList().contains(Province("China", "Guangdong", 150000))) + + var province = database.provinces.first { (it.country eq "China") and (it.province eq "Hebei") } + province.population = 200000 + database.provinces.update(province) + + province = database.provinces.first { (it.country eq "China") and (it.province eq "Hebei") } + assert(province.population == 200000) + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/DatabaseNamingStrategyTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/DatabaseNamingStrategyTest.kt new file mode 100644 index 000000000..76d1b4a01 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/DatabaseNamingStrategyTest.kt @@ -0,0 +1,132 @@ +package org.ktorm.ksp.compiler.parser + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest +import org.ktorm.ksp.compiler.util.CamelCase +import kotlin.test.assertEquals + +class DatabaseNamingStrategyTest : BaseKspTest() { + + @Test + fun testCamelCase() { + assertEquals("abc_def", CamelCase.toLowerSnakeCase("abcDef")) + assertEquals("abc_def", CamelCase.toLowerSnakeCase("AbcDef")) + assertEquals("ABC_DEF", CamelCase.toUpperSnakeCase("abcDef")) + assertEquals("ABC_DEF", CamelCase.toUpperSnakeCase("AbcDef")) + assertEquals("abcDef", CamelCase.toFirstLowerCamelCase("abcDef")) + assertEquals("abcDef", CamelCase.toFirstLowerCamelCase("AbcDef")) + + assertEquals("abc_def", CamelCase.toLowerSnakeCase("ABCDef")) + assertEquals("ABC_DEF", CamelCase.toUpperSnakeCase("ABCDef")) + assertEquals("abcDef", CamelCase.toFirstLowerCamelCase("ABCDef")) + + assertEquals("io_utils", CamelCase.toLowerSnakeCase("IOUtils")) + assertEquals("IO_UTILS", CamelCase.toUpperSnakeCase("IOUtils")) + assertEquals("ioUtils", CamelCase.toFirstLowerCamelCase("IOUtils")) + + assertEquals("pwd_utils", CamelCase.toLowerSnakeCase("PWDUtils")) + assertEquals("PWD_UTILS", CamelCase.toUpperSnakeCase("PWDUtils")) + assertEquals("pwdUtils", CamelCase.toFirstLowerCamelCase("PWDUtils")) + + assertEquals("pwd_utils", CamelCase.toLowerSnakeCase("PwdUtils")) + assertEquals("PWD_UTILS", CamelCase.toUpperSnakeCase("PwdUtils")) + assertEquals("pwdUtils", CamelCase.toFirstLowerCamelCase("PwdUtils")) + + assertEquals("test_io", CamelCase.toLowerSnakeCase("testIO")) + assertEquals("TEST_IO", CamelCase.toUpperSnakeCase("testIO")) + assertEquals("testIO", CamelCase.toFirstLowerCamelCase("testIO")) + + assertEquals("test_pwd", CamelCase.toLowerSnakeCase("testPWD")) + assertEquals("TEST_PWD", CamelCase.toUpperSnakeCase("testPWD")) + assertEquals("testPWD", CamelCase.toFirstLowerCamelCase("testPWD")) + + assertEquals("test_pwd", CamelCase.toLowerSnakeCase("testPwd")) + assertEquals("TEST_PWD", CamelCase.toUpperSnakeCase("testPwd")) + assertEquals("testPwd", CamelCase.toFirstLowerCamelCase("testPwd")) + + assertEquals("a2c_count", CamelCase.toLowerSnakeCase("A2CCount")) + assertEquals("A2C_COUNT", CamelCase.toUpperSnakeCase("A2CCount")) + assertEquals("a2cCount", CamelCase.toFirstLowerCamelCase("A2CCount")) + } + + @Test + fun testDefaultNaming() = runKotlin(""" + @Table + interface UserProfile: Entity { + @PrimaryKey + var id: Int + var publicEmail: String + var profilePicture: Int + @References + var company: Company + } + + @Table + interface Company: Entity { + @PrimaryKey + var id: Int + var name: String + } + + fun run() { + assert(Companies.tableName == "company") + assert(Companies.columns.map { it.name }.toSet() == setOf("id", "name")) + assert(UserProfiles.tableName == "user_profile") + assert(UserProfiles.columns.map { it.name }.toSet() == setOf("id", "public_email", "profile_picture", "company_id")) + } + """.trimIndent()) + + @Test + fun testUpperCamelCaseNamingByAlias() = runKotlin(""" + @Table + interface UserProfile: Entity { + @PrimaryKey + var id: Int + var publicEmail: String + var profilePicture: Int + @References + var company: Company + } + + @Table + interface Company: Entity { + @PrimaryKey + var id: Int + var name: String + } + + fun run() { + assert(Companies.tableName == "COMPANY") + assert(Companies.columns.map { it.name }.toSet() == setOf("ID", "NAME")) + assert(UserProfiles.tableName == "USER_PROFILE") + assert(UserProfiles.columns.map { it.name }.toSet() == setOf("ID", "PUBLIC_EMAIL", "PROFILE_PICTURE", "COMPANY_ID")) + } + """.trimIndent(), "ktorm.dbNamingStrategy" to "upper-snake-case") + + @Test + fun testUpperCamelCaseNamingByClassName() = runKotlin(""" + @Table + interface UserProfile: Entity { + @PrimaryKey + var id: Int + var publicEmail: String + var profilePicture: Int + @References + var company: Company + } + + @Table + interface Company: Entity { + @PrimaryKey + var id: Int + var name: String + } + + fun run() { + assert(Companies.tableName == "COMPANY") + assert(Companies.columns.map { it.name }.toSet() == setOf("ID", "NAME")) + assert(UserProfiles.tableName == "USER_PROFILE") + assert(UserProfiles.columns.map { it.name }.toSet() == setOf("ID", "PUBLIC_EMAIL", "PROFILE_PICTURE", "COMPANY_ID")) + } + """.trimIndent(), "ktorm.dbNamingStrategy" to "org.ktorm.ksp.compiler.util.UpperSnakeCaseDatabaseNamingStrategy") +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/MetadataParserTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/MetadataParserTest.kt new file mode 100644 index 000000000..38b9f7dbb --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/MetadataParserTest.kt @@ -0,0 +1,306 @@ +package org.ktorm.ksp.compiler.parser + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +/** + * Created by vince at Apr 16, 2023. + */ +class MetadataParserTest : BaseKspTest() { + + @Test + fun testEnumClass() = kspFailing("Gender should be a class or interface but actually ENUM_CLASS.", """ + @Table + enum class Gender { MALE, FEMALE } + """.trimIndent()) + + @Test + fun testInterfaceNotExtendingEntity() = kspFailing("User should extend from org.ktorm.entity.Entity.", """ + @Table + interface User { + val id: Int + val name: String + } + """.trimIndent()) + + @Test + fun testAbstractClass() = kspFailing("User cannot be an abstract class.", """ + @Table + abstract class User { + var id: Int = 0 + var name: String = "" + } + """.trimIndent()) + + @Test + fun testClassIgnoreProperties() = runKotlin(""" + @Table(ignoreProperties = ["name"]) + class User( + val id: Int, + val name: String? = null + ) + + fun run() { + assert(Users.columns.map { it.name }.toSet() == setOf("id")) + } + """.trimIndent()) + + @Test + fun testInterfaceIgnoreProperties() = runKotlin(""" + @Table(ignoreProperties = ["name"]) + interface User : Entity { + val id: Int + val name: String + } + + fun run() { + assert(Users.columns.map { it.name }.toSet() == setOf("id")) + } + """.trimIndent()) + + @Test + fun testClassIgnoreAnnotation() = runKotlin(""" + @Table + class User( + val id: Int, + @Ignore + val name: String? = null + ) + + fun run() { + assert(Users.columns.map { it.name }.toSet() == setOf("id")) + } + """.trimIndent()) + + @Test + fun testInterfaceIgnoreAnnotation() = runKotlin(""" + @Table + interface User : Entity { + val id: Int + @Ignore + val name: String + } + + fun run() { + assert(Users.columns.map { it.name }.toSet() == setOf("id")) + } + """.trimIndent()) + + @Test + fun testClassPropertiesWithoutBackingField() = runKotlin(""" + @Table + class User(val id: Int) { + val name: String get() = "vince" + } + + fun run() { + assert(Users.columns.map { it.name }.toSet() == setOf("id")) + } + """.trimIndent()) + + @Test + fun testClassPropertiesOrder() = runKotlin(""" + @Table + class User(var id: Int) { + var name: String? = null + } + + fun run() { + assert(Users.columns.map { it.name }[0] == "id") + assert(Users.columns.map { it.name }[1] == "name") + } + """.trimIndent()) + + @Test + fun testInterfaceNonAbstractProperties() = runKotlin(""" + @Table + interface User : Entity { + val id: Int + val name: String get() = "vince" + } + + fun run() { + assert(Users.columns.map { it.name }.toSet() == setOf("id")) + } + """.trimIndent()) + + @Test + fun testSqlTypeInferError() = kspFailing("Parse sqlType error for property User.name: cannot infer sqlType, please specify manually.", """ + @Table + interface User : Entity { + val name: java.lang.StringBuilder + } + """.trimIndent()) + + @Test + fun testAbstractSqlType() = kspFailing("Parse sqlType error for property User.ex: the sqlType class cannot be abstract.", """ + @Table + interface User : Entity { + @PrimaryKey + var id: Int + var name: String + @Column(sqlType = ExSqlType::class) + var ex: Map + } + + abstract class ExSqlType : org.ktorm.schema.SqlType(Types.OTHER, "json") + """.trimIndent()) + + @Test + fun testSqlTypeWithoutConstructor() = kspFailing("Parse sqlType error for property User.ex: the sqlType should be a Kotlin singleton object or a normal class with a constructor that accepts a single org.ktorm.schema.TypeReference argument.", """ + @Table + interface User : Entity { + @PrimaryKey + var id: Int + var name: String + @Column(sqlType = ExSqlType::class) + var ex: Map + } + + class ExSqlType : org.ktorm.schema.SqlType(Types.OTHER, "json") { + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: Any) { + TODO("not implemented.") + } + + override fun doGetResult(rs: ResultSet, index: Int): Any? { + TODO("not implemented.") + } + } + """.trimIndent()) + + @Test + fun testReferencesWithColumn() = kspFailing("Parse ref column error for property User.profile: @Column and @References cannot be used together.", """ + @Table + interface User : Entity { + val id: Int + @References + @Column + val profile: Profile + } + + @Table + interface Profile : Entity { + val id: Int + val name: String + } + """.trimIndent()) + + @Test + fun testReferencesFromClassEntity() = kspFailing("Parse ref column error for property User.profile: @References only allowed in interface-based entities", """ + @Table + class User( + val id: Int, + @References + val profile: Profile + ) + + @Table + interface Profile : Entity { + val id: Int + val name: String + } + """.trimIndent()) + + @Test + fun testReferencesToClassEntity() = kspFailing("Parse ref column error for property User.profile: the referenced entity class must be an interface", """ + @Table + interface User : Entity { + val id: Int + @References + val profile: Profile + } + + @Table + class Profile( + val id: Int, + val name: String + ) + """.trimIndent()) + + @Test + fun testReferencesToNonTableClass() = kspFailing("Parse ref column error for property User.profile: the referenced entity must be annotated with @Table", """ + @Table + interface User : Entity { + val id: Int + @References + val profile: Profile + } + + interface Profile : Entity { + val id: Int + val name: String + } + """.trimIndent()) + + @Test + fun testReferencesNoPrimaryKeys() = kspFailing("Parse ref column error for property User.profile: the referenced table doesn't have a primary key", """ + @Table + interface User : Entity { + val id: Int + @References + val profile: Profile + } + + @Table + interface Profile : Entity { + val id: Int + val name: String + } + """.trimIndent()) + + @Test + fun testReferencesWithCompoundPrimaryKeys() = kspFailing("Parse ref column error for property User.profile: the referenced table cannot have compound primary keys", """ + @Table + interface User : Entity { + val id: Int + @References + val profile: Profile + } + + @Table + interface Profile : Entity { + @PrimaryKey + val id: Int + @PrimaryKey + val name: String + } + """.trimIndent()) + + @Test + fun testCircularReference() = kspFailing("Circular reference is not allowed, current table: User, reference route: Profile --> Operator --> User", """ + @Table + interface User : Entity { + @PrimaryKey + val id: Int + @References + val profile: Profile + } + + @Table + interface Profile : Entity { + @PrimaryKey + val id: Int + @References + val operator: Operator + } + + @Table + interface Operator : Entity { + @PrimaryKey + val id: Int + @References + val user: User + } + """.trimIndent()) + + @Test + fun testSelfReference() = kspFailing("Circular reference is not allowed, current table: User, reference route: User", """ + @Table + interface User : Entity { + @PrimaryKey + val id: Int + @References + val manager: User + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/SqlTypeTest.kt b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/SqlTypeTest.kt new file mode 100644 index 000000000..8b50e1fa4 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/kotlin/org/ktorm/ksp/compiler/parser/SqlTypeTest.kt @@ -0,0 +1,131 @@ +package org.ktorm.ksp.compiler.parser + +import org.junit.Test +import org.ktorm.ksp.compiler.BaseKspTest + +class SqlTypeTest : BaseKspTest() { + + @Test + fun testDefaultSqlType() = runKotlin(""" + @Table + data class User( + val int: Int, + val string: String, + val boolean: Boolean, + val long: Long, + val short: Short, + val double: Double, + val float: Float, + val bigDecimal: BigDecimal, + val date: java.sql.Date, + val time: Time, + val timestamp: Timestamp, + val localDateTime: LocalDateTime, + val localDate: LocalDate, + val localTime: LocalTime, + val monthDay: MonthDay, + val yearMonth: YearMonth, + val year: Year, + val instant: Instant, + val uuid: UUID, + val byteArray: ByteArray, + val gender: Gender + ) + + enum class Gender { + MALE, + FEMALE + } + + fun run() { + assert(Users.int.sqlType == org.ktorm.schema.IntSqlType) + assert(Users.string.sqlType == org.ktorm.schema.VarcharSqlType) + assert(Users.boolean.sqlType == org.ktorm.schema.BooleanSqlType) + assert(Users.long.sqlType == org.ktorm.schema.LongSqlType) + assert(Users.short.sqlType == org.ktorm.schema.ShortSqlType) + assert(Users.double.sqlType == org.ktorm.schema.DoubleSqlType) + assert(Users.float.sqlType == org.ktorm.schema.FloatSqlType) + assert(Users.bigDecimal.sqlType == org.ktorm.schema.DecimalSqlType) + assert(Users.date.sqlType == org.ktorm.schema.DateSqlType) + assert(Users.time.sqlType == org.ktorm.schema.TimeSqlType) + assert(Users.timestamp.sqlType == org.ktorm.schema.TimestampSqlType) + assert(Users.localDateTime.sqlType == org.ktorm.schema.LocalDateTimeSqlType) + assert(Users.localDate.sqlType == org.ktorm.schema.LocalDateSqlType) + assert(Users.localTime.sqlType == org.ktorm.schema.LocalTimeSqlType) + assert(Users.monthDay.sqlType == org.ktorm.schema.MonthDaySqlType) + assert(Users.yearMonth.sqlType == org.ktorm.schema.YearMonthSqlType) + assert(Users.year.sqlType == org.ktorm.schema.YearSqlType) + assert(Users.instant.sqlType == org.ktorm.schema.InstantSqlType) + assert(Users.uuid.sqlType == org.ktorm.schema.UuidSqlType) + assert(Users.byteArray.sqlType == org.ktorm.schema.BytesSqlType) + assert(Users.gender.sqlType is org.ktorm.schema.EnumSqlType<*>) + } + """.trimIndent()) + + @Test + fun testCustomSqlType() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + @Column(sqlType = UsernameSqlType::class) + var username: Username, + var age: Int, + var gender: Int + ) + + data class Username( + val firstName:String, + val lastName:String + ) + + object UsernameSqlType : org.ktorm.schema.SqlType(Types.VARCHAR, "varchar") { + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: Username) { + ps.setString(index, parameter.firstName + "#" + parameter.lastName) + } + + override fun doGetResult(rs: ResultSet, index: Int): Username? { + val (firstName, lastName) = rs.getString(index)?.split("#") ?: return null + return Username(firstName, lastName) + } + } + + fun run() { + assert(Users.username.sqlType == UsernameSqlType) + assert(Users.username.sqlType.typeCode == Types.VARCHAR) + assert(Users.username.sqlType.typeName == "varchar") + + database.users.add(User(100, Username("Vincent", "Lau"), 28, 0)) + assert(database.users.first { it.id eq 100 } == User(100, Username("Vincent", "Lau"), 28, 0)) + } + """.trimIndent()) + + @Test + fun testCustomSqlTypeWithConstructor() = runKotlin(""" + @Table + data class User( + @PrimaryKey + var id: Int, + var username: String, + var age: Int, + @Column(sqlType = ExSqlType::class) + var ex: Map + ) + + class ExSqlType(val typeRef: org.ktorm.schema.TypeReference) : org.ktorm.schema.SqlType(Types.OTHER, "json") { + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: C) { + TODO("not implemented.") + } + + override fun doGetResult(rs: ResultSet, index: Int): C? { + TODO("not implemented.") + } + } + + fun run() { + assert(Users.ex.sqlType::class.simpleName == "ExSqlType") + assert(Users.ex.sqlType.typeCode == Types.OTHER) + assert(Users.ex.sqlType.typeName == "json") + } + """.trimIndent()) +} diff --git a/ktorm-ksp-compiler/src/test/resources/META-INF/services/org.ktorm.ksp.spi.ExtCodeGenerator b/ktorm-ksp-compiler/src/test/resources/META-INF/services/org.ktorm.ksp.spi.ExtCodeGenerator new file mode 100644 index 000000000..b161c3299 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/resources/META-INF/services/org.ktorm.ksp.spi.ExtCodeGenerator @@ -0,0 +1 @@ +org.ktorm.ksp.compiler.generator.ExtCodeGeneratorTest diff --git a/ktorm-ksp-compiler/src/test/resources/drop-ksp-data.sql b/ktorm-ksp-compiler/src/test/resources/drop-ksp-data.sql new file mode 100644 index 000000000..08b01358e --- /dev/null +++ b/ktorm-ksp-compiler/src/test/resources/drop-ksp-data.sql @@ -0,0 +1,4 @@ +drop table if exists "user"; +drop table if exists "employee"; +drop table if exists "department"; +drop table if exists "province"; diff --git a/ktorm-ksp-compiler/src/test/resources/init-ksp-data.sql b/ktorm-ksp-compiler/src/test/resources/init-ksp-data.sql new file mode 100644 index 000000000..462c05b39 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/resources/init-ksp-data.sql @@ -0,0 +1,55 @@ +create table "user" +( + "id" int not null primary key auto_increment, + "username" varchar(128) not null, + "age" int not null, + "gender" int not null default 0, + "phone" varchar(128) null +); + + +insert into "user"("username", "age", "gender") +values ('jack', 20, 0), + ('lucy', 22, 1), + ('mike', 22, 0); + + +create table "employee" +( + "id" int not null primary key auto_increment, + "name" varchar(128) not null, + "job" varchar(128) not null, + "manager_id" int null, + "hire_date" date not null, + "salary" bigint not null, + "department_id" int not null +); + + +insert into "employee"("name", "job", "manager_id", "hire_date", "salary", "department_id") +values ('vince', 'engineer', null, '2018-01-01', 100, 1), + ('marry', 'trainee', 1, '2019-01-01', 50, 1), + ('tom', 'director', null, '2018-01-01', 200, 2), + ('penny', 'assistant', 3, '2019-01-01', 100, 2); + +create table "department"( + "id" int not null primary key auto_increment, + "name" varchar(128) not null, + "location" varchar(128) not null, + "mixedCase" varchar(128) +); + +insert into "department"("name", "location") values ('tech', 'Guangzhou'); +insert into "department"("name", "location") values ('finance', 'Beijing'); + +create table "province" +( + "country" varchar(128) not null, + "province" varchar(128) not null, + "population" int not null, + primary key ("country", "province") +); + +insert into "province"("country", "province", "population") +values ('China', 'Hebei', 130000), + ('China', 'Henan', 140000); diff --git a/ktorm-ksp-compiler/src/test/resources/simplelogger.properties b/ktorm-ksp-compiler/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..08dada486 --- /dev/null +++ b/ktorm-ksp-compiler/src/test/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.logFile=System.err +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.log.org.ktorm.database=trace +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=false +org.slf4j.simpleLogger.levelInBrackets=true \ No newline at end of file diff --git a/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts b/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts new file mode 100644 index 000000000..86f5b1ce5 --- /dev/null +++ b/ktorm-ksp-spi/ktorm-ksp-spi.gradle.kts @@ -0,0 +1,11 @@ + +plugins { + id("ktorm.base") + id("ktorm.publish") + id("ktorm.source-header-check") +} + +dependencies { + api("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.13") + api("com.squareup:kotlinpoet-ksp:1.11.0") +} diff --git a/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/CodingNamingStrategy.kt b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/CodingNamingStrategy.kt new file mode 100644 index 000000000..ea80147ab --- /dev/null +++ b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/CodingNamingStrategy.kt @@ -0,0 +1,51 @@ +/* + * 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.spi + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration + +/** + * Naming strategy for Kotlin symbols in the generated code. + */ +public interface CodingNamingStrategy { + + /** + * Generate the table class name. + */ + public fun getTableClassName(c: KSClassDeclaration): String + + /** + * Generate the entity sequence name. + */ + public fun getEntitySequenceName(c: KSClassDeclaration): String + + /** + * Generate the column property name. + */ + public fun getColumnPropertyName(c: KSClassDeclaration, prop: KSPropertyDeclaration): String + + /** + * Generate the reference column property name. + */ + public fun getRefColumnPropertyName(c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata): String + + /** + * Generate the name of the referenced table property in the Refs wrapper class. + */ + public fun getRefTablePropertyName(c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata): String +} diff --git a/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/ColumnMetadata.kt b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/ColumnMetadata.kt new file mode 100644 index 000000000..8e01f9ad6 --- /dev/null +++ b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/ColumnMetadata.kt @@ -0,0 +1,71 @@ +/* + * 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.spi + +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSType + +/** + * Column definition metadata. + */ +public data class ColumnMetadata( + + /** + * The annotated entity property of the column. + */ + val entityProperty: KSPropertyDeclaration, + + /** + * The belonging table. + */ + val table: TableMetadata, + + /** + * The name of the column. + */ + val name: String, + + /** + * Check if the column is a primary key. + */ + val isPrimaryKey: Boolean, + + /** + * The SQL type of the column. + */ + val sqlType: KSType, + + /** + * Check if the column is a reference column. + */ + val isReference: Boolean, + + /** + * The referenced table of the column. + */ + val referenceTable: TableMetadata?, + + /** + * The name of the corresponding column property in the generated table class. + */ + val columnPropertyName: String, + + /** + * The name of the corresponding referenced table property in the Refs wrapper class. + */ + val refTablePropertyName: String? +) diff --git a/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/DatabaseNamingStrategy.kt b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/DatabaseNamingStrategy.kt new file mode 100644 index 000000000..af41ccb97 --- /dev/null +++ b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/DatabaseNamingStrategy.kt @@ -0,0 +1,41 @@ +/* + * 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.spi + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration + +/** + * Naming strategy for database tables and columns. + */ +public interface DatabaseNamingStrategy { + + /** + * Generate the table name. + */ + public fun getTableName(c: KSClassDeclaration): String + + /** + * Generate the column name. + */ + public fun getColumnName(c: KSClassDeclaration, prop: KSPropertyDeclaration): String + + /** + * Generate the reference column name. + */ + public fun getRefColumnName(c: KSClassDeclaration, prop: KSPropertyDeclaration, ref: TableMetadata): String +} diff --git a/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/ExtCodeGenerator.kt b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/ExtCodeGenerator.kt new file mode 100644 index 000000000..c6b57c28a --- /dev/null +++ b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/ExtCodeGenerator.kt @@ -0,0 +1,49 @@ +/* + * 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.spi + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec + +/** + * Ktorm KSP code generator interface for third-party extensions. + */ +public interface ExtCodeGenerator { + + /** + * Generate types for the [table] in the corresponding resulting file. + */ + public fun generateTypes(table: TableMetadata, environment: SymbolProcessorEnvironment): List { + return emptyList() + } + + /** + * Generate top-level properties for the [table] in the corresponding resulting file. + */ + public fun generateProperties(table: TableMetadata, environment: SymbolProcessorEnvironment): List { + return emptyList() + } + + /** + * Generate top-level functions for the [table] in the corresponding resulting file. + */ + public fun generateFunctions(table: TableMetadata, environment: SymbolProcessorEnvironment): List { + return emptyList() + } +} 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 new file mode 100644 index 000000000..25ed0b159 --- /dev/null +++ b/ktorm-ksp-spi/src/main/kotlin/org/ktorm/ksp/spi/TableMetadata.kt @@ -0,0 +1,70 @@ +/* + * 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.spi + +import com.google.devtools.ksp.symbol.KSClassDeclaration + +/** + * Table definition metadata. + */ +public data class TableMetadata( + + /** + * The annotated entity class of the table. + */ + val entityClass: KSClassDeclaration, + + /** + * The name of the table. + */ + val name: String, + + /** + * The alias of the table. + */ + val alias: String?, + + /** + * The catalog of the table. + */ + val catalog: String?, + + /** + * The schema of the table. + */ + val schema: String?, + + /** + * The name of the corresponding table class in the generated code. + */ + val tableClassName: String, + + /** + * The name of the corresponding entity sequence in the generated code. + */ + val entitySequenceName: String, + + /** + * Properties that should be ignored for generating column definitions. + */ + val ignoreProperties: Set, + + /** + * Columns in the table. + */ + val columns: List +) diff --git a/ktorm-support-mysql/ktorm-support-mysql.gradle.kts b/ktorm-support-mysql/ktorm-support-mysql.gradle.kts index 933027468..7ec63785d 100644 --- a/ktorm-support-mysql/ktorm-support-mysql.gradle.kts +++ b/ktorm-support-mysql/ktorm-support-mysql.gradle.kts @@ -1,12 +1,15 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { api(project(":ktorm-core")) testImplementation(project(":ktorm-core", configuration = "testOutput")) testImplementation(project(":ktorm-jackson")) - testImplementation("org.testcontainers:mysql:1.15.1") + testImplementation("org.testcontainers:mysql:1.19.7") testImplementation("mysql:mysql-connector-java:8.0.23") } diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/BulkInsert.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/BulkInsert.kt index 63b3719ed..5f04f19ae 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/BulkInsert.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/BulkInsert.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/DefaultValue.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/DefaultValue.kt index e368503f9..451f2acbf 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/DefaultValue.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/DefaultValue.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Functions.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Functions.kt index 8b9f4669b..14f1217cb 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Functions.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Functions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Global.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Global.kt index 44cd6c7f9..81cc1728e 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Global.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Global.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/InsertOrUpdate.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/InsertOrUpdate.kt index 1a562c7d7..08360347a 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/InsertOrUpdate.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/InsertOrUpdate.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt index 11d56e6cd..63a85cdc9 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MatchAgainst.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MatchAgainst.kt index bf01d1734..b89a9143c 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MatchAgainst.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MatchAgainst.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 628c87397..e996fd5e6 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlExpressionVisitor.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlExpressionVisitor.kt index b90c87763..9b63ed1ac 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlExpressionVisitor.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlExpressionVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlFormatter.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlFormatter.kt index d98598828..e8866060f 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlFormatter.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/NaturalJoin.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/NaturalJoin.kt index af80fb112..e414e5bd1 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/NaturalJoin.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/NaturalJoin.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-oracle/ktorm-support-oracle.gradle.kts b/ktorm-support-oracle/ktorm-support-oracle.gradle.kts index ec6b3a257..9cc0fab84 100644 --- a/ktorm-support-oracle/ktorm-support-oracle.gradle.kts +++ b/ktorm-support-oracle/ktorm-support-oracle.gradle.kts @@ -1,11 +1,14 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { api(project(":ktorm-core")) testImplementation(project(":ktorm-core", configuration = "testOutput")) - testImplementation("org.testcontainers:oracle-xe:1.15.1") + testImplementation("org.testcontainers:oracle-xe:1.19.7") testImplementation(files("lib/ojdbc6-11.2.0.3.jar")) } diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt index 80bc3706f..b5517119e 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleFormatter.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleFormatter.kt index 7e14f624c..214e63e3b 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleFormatter.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/BaseOracleTest.kt b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/BaseOracleTest.kt index ac7d5b1df..7b6ccdce2 100644 --- a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/BaseOracleTest.kt +++ b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/BaseOracleTest.kt @@ -8,14 +8,7 @@ import org.testcontainers.containers.OracleContainer abstract class BaseOracleTest : BaseTest() { override fun init() { - database = Database.connect( - url = container.jdbcUrl, - driver = container.driverClassName, - user = container.username, - password = container.password, - alwaysQuoteIdentifiers = true - ) - + database = Database.connect(jdbcUrl, driverClassName, username, password, alwaysQuoteIdentifiers = true) execSqlScript("init-oracle-data.sql") } @@ -23,11 +16,36 @@ abstract class BaseOracleTest : BaseTest() { execSqlScript("drop-oracle-data.sql") } + /** + * Unfortunately Oracle databases aren’t compatible with the new Apple Silicon CPU architecture, + * so if you are using a brand-new MacBook, you need to install colima. + * + * 1. Installation: https://github.com/abiosoft/colima#installation + * 2. Run Colima with the command: `colima start --arch x86_64 --cpu 2 --memory 4 --disk 16 --network-address` + * 3. Set env vars like below: + * + * ```sh + * export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock + * export TESTCONTAINERS_HOST_OVERRIDE=$(colima ls -j | jq -r '.address') + * export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" + * ``` + * + * See https://java.testcontainers.org/supported_docker_environment/#colima + */ companion object { @JvmField @ClassRule - val container = OracleContainer("zerda/oracle-database:11.2.0.2-xe") - // At least 1 GB memory is required by Oracle. - .withCreateContainerCmdModifier { cmd -> cmd.hostConfig?.withShmSize((1 * 1024 * 1024 * 1024).toLong()) } + val container: OracleContainer + = OracleContainer("gvenzl/oracle-xe:11.2.0.2") + .usingSid() + .withCreateContainerCmdModifier { cmd -> cmd.hostConfig?.withShmSize((1 * 1024 * 1024 * 1024).toLong()) } + + val jdbcUrl: String get() = container.jdbcUrl + + val driverClassName: String get() = container.driverClassName + + val username: String get() = container.username + + val password: String get() = container.password } } \ No newline at end of file diff --git a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/CommonTest.kt b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/CommonTest.kt index e375e29e5..fe846429d 100644 --- a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/CommonTest.kt +++ b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/CommonTest.kt @@ -106,7 +106,7 @@ class CommonTest : BaseOracleTest() { @Test fun testSchema() { - val t = object : Table("t_department", schema = container.username.uppercase()) { + val t = object : Table("t_department", schema = username.uppercase()) { val id = int("id").primaryKey().bindTo { it.id } val name = varchar("name").bindTo { it.name } } diff --git a/ktorm-support-postgresql/ktorm-support-postgresql.gradle.kts b/ktorm-support-postgresql/ktorm-support-postgresql.gradle.kts index fc93a6966..6ce10fd3a 100644 --- a/ktorm-support-postgresql/ktorm-support-postgresql.gradle.kts +++ b/ktorm-support-postgresql/ktorm-support-postgresql.gradle.kts @@ -1,13 +1,16 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { api(project(":ktorm-core")) testImplementation(project(":ktorm-core", configuration = "testOutput")) testImplementation(project(":ktorm-jackson")) - testImplementation("org.testcontainers:postgresql:1.15.1") + testImplementation("org.testcontainers:postgresql:1.19.7") testImplementation("org.postgresql:postgresql:42.2.5") testImplementation("com.zaxxer:HikariCP:4.0.3") testImplementation("com.mchange:c3p0:0.9.5.5") diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 328a19be9..927b71477 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/DefaultValue.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/DefaultValue.kt index 29fdb80a7..9d8ca157c 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/DefaultValue.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/DefaultValue.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/EarthDistance.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/EarthDistance.kt index 1222cfd70..ab2b2ea7d 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/EarthDistance.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/EarthDistance.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Functions.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Functions.kt new file mode 100644 index 000000000..24234785e --- /dev/null +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Functions.kt @@ -0,0 +1,234 @@ +/* + * 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.support.postgresql + +import org.ktorm.expression.ArgumentExpression +import org.ktorm.expression.FunctionExpression +import org.ktorm.schema.* + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("shortArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("shortArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: Short, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, ArgumentExpression(value, ShortSqlType), offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("shortArrayPosition") +public fun arrayPosition( + array: ShortArray, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(ArgumentExpression(array, ShortArraySqlType), value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("intArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("intArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: Int, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, ArgumentExpression(value, IntSqlType), offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("intArrayPosition") +public fun arrayPosition( + array: IntArray, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(ArgumentExpression(array, IntArraySqlType), value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("longArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("longArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: Long, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, ArgumentExpression(value, LongSqlType), offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("longArrayPosition") +public fun arrayPosition( + array: LongArray, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(ArgumentExpression(array, LongArraySqlType), value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("booleanArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("booleanArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: Boolean, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, ArgumentExpression(value, BooleanSqlType), offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("booleanArrayPosition") +public fun arrayPosition( + array: BooleanArray, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(ArgumentExpression(array, BooleanArraySqlType), value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("textArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, value, offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("textArrayPosition") +public fun arrayPosition( + array: ColumnDeclaring, value: String, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(array, ArgumentExpression(value, VarcharSqlType), offset) +} + +/** + * Returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present. + * If the third argument is given, the search begins at that subscript. The array must be one-dimensional. + * + * array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') → 2 + */ +@JvmName("textArrayPosition") +public fun arrayPosition( + array: TextArray, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + return arrayPositionImpl(ArgumentExpression(array, TextArraySqlType), value, offset) +} + +/** + * array_position implementation. + */ +private fun arrayPositionImpl( + array: ColumnDeclaring, value: ColumnDeclaring, offset: Int? = null +): FunctionExpression { + // array_position(array, value[, offset]) + return FunctionExpression( + functionName = "array_position", + arguments = listOfNotNull( + array.asExpression(), + value.asExpression(), + offset?.let { ArgumentExpression(it, IntSqlType) } + ), + sqlType = IntSqlType + ) +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt index 3b4279b53..9596c5fa3 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Global.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/HStore.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/HStore.kt index eb824cf87..8b4217350 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/HStore.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/HStore.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/ILike.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/ILike.kt index d0c1e11e3..c09b34bf9 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/ILike.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/ILike.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index f48eaa345..707672dc1 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt index 81a8441fb..e50715981 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 419932146..90fbccbaa 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlExpressionVisitor.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlExpressionVisitor.kt index 6a28b95da..13e04cc8f 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlExpressionVisitor.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlExpressionVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlFormatter.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlFormatter.kt index c40454ff2..671f6d636 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlFormatter.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt index aef420611..2ddb7081b 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. @@ -24,6 +24,174 @@ import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.Types +/** + * Define a column typed [ShortArraySqlType]. + */ +public fun BaseTable<*>.shortArray(name: String): Column { + return registerColumn(name, ShortArraySqlType) +} + +/** + * [SqlType] implementation represents PostgreSQL `smallint[]` type. + */ +public object ShortArraySqlType : SqlType(Types.ARRAY, "smallint[]") { + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: ShortArray) { + ps.setObject(index, parameter) + } + + @Suppress("UNCHECKED_CAST") + override fun doGetResult(rs: ResultSet, index: Int): ShortArray? { + val sqlArray = rs.getArray(index) ?: return null + try { + val objectArray = sqlArray.array as Array? + return objectArray?.map { it as Short }?.toShortArray() + } finally { + sqlArray.free() + } + } +} + +/** + * Define a column typed [IntArraySqlType]. + */ +public fun BaseTable<*>.intArray(name: String): Column { + return registerColumn(name, IntArraySqlType) +} + +/** + * [SqlType] implementation represents PostgreSQL `integer[]` type. + */ +public object IntArraySqlType : SqlType(Types.ARRAY, "integer[]") { + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: IntArray) { + ps.setObject(index, parameter) + } + + @Suppress("UNCHECKED_CAST") + override fun doGetResult(rs: ResultSet, index: Int): IntArray? { + val sqlArray = rs.getArray(index) ?: return null + try { + val objectArray = sqlArray.array as Array? + return objectArray?.map { it as Int }?.toIntArray() + } finally { + sqlArray.free() + } + } +} + +/** + * Define a column typed [LongArraySqlType]. + */ +public fun BaseTable<*>.longArray(name: String): Column { + return registerColumn(name, LongArraySqlType) +} + +/** + * [SqlType] implementation represents PostgreSQL `bigint[]` type. + */ +public object LongArraySqlType : SqlType(Types.ARRAY, "bigint[]") { + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: LongArray) { + ps.setObject(index, parameter) + } + + @Suppress("UNCHECKED_CAST") + override fun doGetResult(rs: ResultSet, index: Int): LongArray? { + val sqlArray = rs.getArray(index) ?: return null + try { + val objectArray = sqlArray.array as Array? + return objectArray?.map { it as Long }?.toLongArray() + } finally { + sqlArray.free() + } + } +} + +/** + * Define a column typed [FloatArraySqlType]. + */ +public fun BaseTable<*>.floatArray(name: String): Column { + return registerColumn(name, FloatArraySqlType) +} + +/** + * [SqlType] implementation represents PostgreSQL `real[]` type. + */ +public object FloatArraySqlType : SqlType(Types.FLOAT, "real[]") { + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: FloatArray) { + ps.setObject(index, parameter) + } + + @Suppress("UNCHECKED_CAST") + override fun doGetResult(rs: ResultSet, index: Int): FloatArray? { + val sqlArray = rs.getArray(index) ?: return null + try { + val objectArray = sqlArray.array as Array? + return objectArray?.map { it as Float }?.toFloatArray() + } finally { + sqlArray.free() + } + } +} + +/** + * Define a column typed [DoubleArraySqlType]. + */ +public fun BaseTable<*>.doubleArray(name: String): Column { + return registerColumn(name, DoubleArraySqlType) +} + +/** + * [SqlType] implementation represents PostgreSQL `float8[]` type. + */ +public object DoubleArraySqlType : SqlType(Types.ARRAY, "float8[]") { + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: DoubleArray) { + ps.setObject(index, parameter) + } + + @Suppress("UNCHECKED_CAST") + override fun doGetResult(rs: ResultSet, index: Int): DoubleArray? { + val sqlArray = rs.getArray(index) ?: return null + try { + val objectArray = sqlArray.array as Array? + return objectArray?.map { it as Double }?.toDoubleArray() + } finally { + sqlArray.free() + } + } +} + +/** + * Define a column typed [BooleanArraySqlType]. + */ +public fun BaseTable<*>.booleanArray(name: String): Column { + return registerColumn(name, BooleanArraySqlType) +} + +/** + * [SqlType] implementation represents PostgreSQL `boolean[]` type. + */ +public object BooleanArraySqlType : SqlType(Types.ARRAY, "boolean[]") { + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: BooleanArray) { + ps.setObject(index, parameter) + } + + @Suppress("UNCHECKED_CAST") + override fun doGetResult(rs: ResultSet, index: Int): BooleanArray? { + val sqlArray = rs.getArray(index) ?: return null + try { + val objectArray = sqlArray.array as Array? + return objectArray?.map { it as Boolean }?.toBooleanArray() + } finally { + sqlArray.free() + } + } +} + /** * Represent values of PostgreSQL `text[]` SQL type. */ diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/ArraysTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/ArraysTest.kt new file mode 100644 index 000000000..b3bca85be --- /dev/null +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/ArraysTest.kt @@ -0,0 +1,62 @@ +package org.ktorm.support.postgresql + +import org.junit.Test +import org.ktorm.dsl.* +import org.ktorm.entity.tupleOf +import org.ktorm.schema.Table +import org.ktorm.schema.int + +/** + * Created by vince at Sep 09, 2023. + */ +class ArraysTest : BasePostgreSqlTest() { + + object Arrays : Table("t_array") { + val id = int("id").primaryKey() + val shorts = shortArray("shorts") + val ints = intArray("ints") + val longs = longArray("longs") + val floats = floatArray("floats") + val doubles = doubleArray("doubles") + val booleans = booleanArray("booleans") + val texts = textArray("texts") + } + + @Test + fun testArrays() { + database.insert(Arrays) { + set(it.shorts, shortArrayOf(1, 2, 3, 4)) + set(it.ints, intArrayOf(1, 2, 3, 4)) + set(it.longs, longArrayOf(1, 2, 3, 4)) + set(it.floats, floatArrayOf(1.0F, 2.0F, 3.0F, 4.0F)) + set(it.doubles, doubleArrayOf(1.0, 2.0, 3.0, 4.0)) + set(it.booleans, booleanArrayOf(false, true)) + set(it.texts, arrayOf("1", "2", "3", "4")) + } + + val results = database + .from(Arrays) + .select(Arrays.columns) + .where(Arrays.id eq 1) + .map { row -> + tupleOf( + row[Arrays.shorts], + row[Arrays.ints], + row[Arrays.longs], + row[Arrays.floats], + row[Arrays.doubles], + row[Arrays.booleans], + row[Arrays.texts] + ) + } + + val (shorts, ints, longs, floats, doubles, booleans, texts) = results[0] + assert(shorts.contentEquals(shortArrayOf(1, 2, 3, 4))) + assert(ints.contentEquals(intArrayOf(1, 2, 3, 4))) + assert(longs.contentEquals(longArrayOf(1, 2, 3, 4))) + assert(floats.contentEquals(floatArrayOf(1.0F, 2.0F, 3.0F, 4.0F))) + assert(doubles.contentEquals(doubleArrayOf(1.0, 2.0, 3.0, 4.0))) + assert(booleans.contentEquals(booleanArrayOf(false, true))) + assert(texts.contentEquals(arrayOf("1", "2", "3", "4"))) + } +} \ No newline at end of file diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/BasePostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/BasePostgreSqlTest.kt index 675a55525..b602dda7b 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/BasePostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/BasePostgreSqlTest.kt @@ -16,7 +16,7 @@ abstract class BasePostgreSqlTest : BaseTest() { execSqlScript("drop-postgresql-data.sql") } - companion object : PostgreSQLContainer("postgres:13-alpine") { + companion object : PostgreSQLContainer("postgres:14-alpine") { init { // Start the container when it's first used. start() @@ -24,4 +24,4 @@ abstract class BasePostgreSqlTest : BaseTest() { Runtime.getRuntime().addShutdownHook(thread(start = false) { stop() }) } } -} \ No newline at end of file +} diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/CommonTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/CommonTest.kt index 018c650c4..8c146c03e 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/CommonTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/CommonTest.kt @@ -148,7 +148,6 @@ class CommonTest : BasePostgreSqlTest() { println(e.message) assert("too long" in e.message!!) } - } enum class Mood { @@ -220,4 +219,4 @@ class CommonTest : BasePostgreSqlTest() { assertEquals("test~~", e1.v) assert(e1.k.isNotEmpty()) } -} \ No newline at end of file +} diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/FunctionsTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/FunctionsTest.kt new file mode 100644 index 000000000..92d49e17d --- /dev/null +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/FunctionsTest.kt @@ -0,0 +1,68 @@ +package org.ktorm.support.postgresql + +import org.junit.Test +import org.ktorm.dsl.* +import org.ktorm.entity.tupleOf +import org.ktorm.schema.Table +import org.ktorm.schema.int +import kotlin.test.assertEquals + +class FunctionsTest : BasePostgreSqlTest() { + + object Arrays : Table("t_array") { + val id = int("id").primaryKey() + val shorts = shortArray("shorts") + val ints = intArray("ints") + val longs = longArray("longs") + val floats = floatArray("floats") + val doubles = doubleArray("doubles") + val booleans = booleanArray("booleans") + val texts = textArray("texts") + } + + @Test + fun testArrayPosition() { + database.insert(Arrays) { + set(it.shorts, shortArrayOf(1, 2, 3, 4)) + set(it.ints, intArrayOf(1, 2, 3, 4)) + set(it.longs, longArrayOf(1, 2, 3, 4)) + set(it.floats, floatArrayOf(1.0F, 2.0F, 3.0F, 4.0F)) + set(it.doubles, doubleArrayOf(1.0, 2.0, 3.0, 4.0)) + set(it.booleans, booleanArrayOf(false, true)) + set(it.texts, arrayOf("1", "2", "3", "4")) + } + + val results = database + .from(Arrays) + .select( + arrayPosition(Arrays.shorts, 2), + arrayPosition(Arrays.ints, 2, 1), + arrayPosition(Arrays.longs, 2), + arrayPosition(Arrays.booleans, true), + arrayPosition(Arrays.texts, "2") + ) + .where(Arrays.id eq 1) + .map { row -> + tupleOf(row.getInt(1), row.getInt(2), row.getInt(3), row.getInt(4), row.getInt(5)) + } + + println(results) + assert(results.size == 1) + + // text[] is one-based, others are zero-based. See https://stackoverflow.com/questions/69649737/postgres-array-positionarray-element-sometimes-0-indexed + assert(results[0] == tupleOf(1, 1, 1, 1, 2)) + } + + @Test + fun testArrayPositionTextArray() { + val namesSorted = database + .from(Employees) + .select() + .orderBy(arrayPosition(arrayOf("tom", "vince", "marry"), Employees.name).asc()) + .map { row -> + row[Employees.name] + } + + assertEquals(listOf("tom", "vince", "marry", "penny"), namesSorted) + } +} diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/HStoreTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/HStoreTest.kt index b4449e694..124a90f02 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/HStoreTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/HStoreTest.kt @@ -4,6 +4,7 @@ import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert import org.junit.Test import org.ktorm.dsl.* +import org.ktorm.entity.tupleOf import org.ktorm.schema.ColumnDeclaring import org.ktorm.schema.Table import org.ktorm.schema.int @@ -13,7 +14,6 @@ class HStoreTest : BasePostgreSqlTest() { object Metadatas : Table("t_metadata") { val id = int("id").primaryKey() val attributes = hstore("attrs") - val numbers = textArray("numbers") } @Test @@ -148,26 +148,4 @@ class HStoreTest : BasePostgreSqlTest() { val updatedAttributes = get { it.attributes } ?: error("Cannot get the attributes!") MatcherAssert.assertThat(updatedAttributes, CoreMatchers.equalTo(emptyMap())) } - - @Test - fun testTextArray() { - database.update(Metadatas) { - set(it.numbers, arrayOf("a", "b")) - where { it.id eq 1 } - } - - val numbers = get { it.numbers } ?: error("Cannot get the numbers!") - MatcherAssert.assertThat(numbers, CoreMatchers.equalTo(arrayOf("a", "b"))) - } - - @Test - fun testTextArrayIsNull() { - database.update(Metadatas) { - set(it.numbers, null) - where { it.id eq 1 } - } - - val numbers = get { it.numbers } - MatcherAssert.assertThat(numbers, CoreMatchers.nullValue()) - } } \ No newline at end of file diff --git a/ktorm-support-postgresql/src/test/resources/drop-postgresql-data.sql b/ktorm-support-postgresql/src/test/resources/drop-postgresql-data.sql index cfe4c8f5b..0bfc34ecf 100644 --- a/ktorm-support-postgresql/src/test/resources/drop-postgresql-data.sql +++ b/ktorm-support-postgresql/src/test/resources/drop-postgresql-data.sql @@ -2,6 +2,7 @@ drop table if exists t_department; drop table if exists t_employee; drop table if exists t_multi_generated_key; drop table if exists t_metadata; +drop table if exists t_array; drop table if exists t_enum; drop type if exists mood; drop table if exists t_json; diff --git a/ktorm-support-postgresql/src/test/resources/init-postgresql-data.sql b/ktorm-support-postgresql/src/test/resources/init-postgresql-data.sql index a4decd685..d627581a3 100644 --- a/ktorm-support-postgresql/src/test/resources/init-postgresql-data.sql +++ b/ktorm-support-postgresql/src/test/resources/init-postgresql-data.sql @@ -27,8 +27,18 @@ create table t_multi_generated_key( create table t_metadata( id serial primary key, - attrs hstore, - numbers text[] + attrs hstore +); + +create table t_array( + id serial primary key, + shorts smallint[], + ints integer[], + longs bigint[], + floats real[], + doubles float8[], + booleans boolean[], + texts text[] ); create type mood as enum ('SAD', 'HAPPY'); @@ -60,8 +70,8 @@ values ('tom', 'director', null, '2018-01-01', 200, 2); insert into t_employee(name, job, manager_id, hire_date, salary, department_id) values ('penny', 'assistant', 3, '2019-01-01', 100, 2); -insert into t_metadata(attrs, numbers) -values ('a=>1, b=>2, c=>NULL'::hstore, array['a', 'b', 'c']); +insert into t_metadata(attrs) +values ('a=>1, b=>2, c=>NULL'::hstore); insert into t_enum(current_mood) values ('HAPPY') diff --git a/ktorm-support-sqlite/ktorm-support-sqlite.gradle.kts b/ktorm-support-sqlite/ktorm-support-sqlite.gradle.kts index b399f4841..d0143ac3e 100644 --- a/ktorm-support-sqlite/ktorm-support-sqlite.gradle.kts +++ b/ktorm-support-sqlite/ktorm-support-sqlite.gradle.kts @@ -1,6 +1,9 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt index 0d3f7fbff..1f4123870 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/BulkInsert.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/Functions.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/Functions.kt index 978d5d62e..e133ffe21 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/Functions.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/Functions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt index 768b8416f..04b2a3719 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/InsertOrUpdate.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt index bc5239f84..eb1b4b22d 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteExpressionVisitor.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteExpressionVisitor.kt index eebd4306c..cbab778c1 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteExpressionVisitor.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteExpressionVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteFormatter.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteFormatter.kt index 0adbc7c9e..cbcc9350c 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteFormatter.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle.kts b/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle.kts index d501c2936..82d9db968 100644 --- a/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle.kts +++ b/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle.kts @@ -1,11 +1,14 @@ plugins { - id("ktorm.module") + id("ktorm.base") + id("ktorm.modularity") + id("ktorm.publish") + id("ktorm.source-header-check") } dependencies { api(project(":ktorm-core")) testImplementation(project(":ktorm-core", configuration = "testOutput")) - testImplementation("org.testcontainers:mssqlserver:1.15.1") + testImplementation("org.testcontainers:mssqlserver:1.19.7") testImplementation("com.microsoft.sqlserver:mssql-jdbc:7.2.2.jre8") } diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt index e0f97de64..7f289d7f4 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerFormatter.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerFormatter.kt index a41ca9061..6cacf1339 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerFormatter.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlTypes.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlTypes.kt index 40a1ed8dd..3039daeff 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlTypes.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlTypes.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * 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. diff --git a/ktorm.version b/ktorm.version index 40c341bdc..fcdb2e109 100644 --- a/ktorm.version +++ b/ktorm.version @@ -1 +1 @@ -3.6.0 +4.0.0 diff --git a/settings.gradle.kts b/settings.gradle.kts index 5d3b92db4..20dab7391 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,11 +1,15 @@ plugins { - id("com.gradle.enterprise") version("3.9") + id("com.gradle.enterprise") version("3.14.1") } include("ktorm-core") include("ktorm-global") include("ktorm-jackson") +include("ktorm-ksp-annotations") +include("ktorm-ksp-compiler") +include("ktorm-ksp-compiler-maven-plugin") +include("ktorm-ksp-spi") include("ktorm-support-mysql") include("ktorm-support-oracle") include("ktorm-support-postgresql")