diff --git a/build.gradle.kts b/build.gradle.kts index e5bf76e940..fb716b30fb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,19 +1,26 @@ import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.report.ReportMergeTask +import org.jetbrains.exposed.gradle.* import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { kotlin("jvm") apply true - id("io.github.gradle-nexus.publish-plugin") apply true id("io.gitlab.arturbosch.detekt") id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.2" + + id("com.avast.gradle.docker-compose") +} + +repositories { + mavenLocal() + mavenCentral() } allprojects { - apply(from = rootProject.file("buildScripts/gradle/checkstyle.gradle.kts")) + configureDetekt() if (this.name != "exposed-tests" && this.name != "exposed-bom" && this != rootProject) { - apply(from = rootProject.file("buildScripts/gradle/publishing.gradle.kts")) + configurePublishing() } } @@ -30,12 +37,14 @@ subprojects { detektPlugins("io.gitlab.arturbosch.detekt", "detekt-formatting", "1.21.0") } tasks.withType().configureEach detekt@{ - enabled = this@subprojects.name !== "exposed-tests" + onlyIf { this@subprojects.name !== "exposed-tests" } + finalizedBy(reportMerge) reportMerge.configure { input.from(this@detekt.xmlReportFile) } } + tasks.withType().configureEach { kotlinOptions { jvmTarget = "1.8" @@ -45,7 +54,104 @@ subprojects { } } -repositories { - mavenLocal() - mavenCentral() +subprojects { + if (name == "exposed-bom") return@subprojects + + apply(plugin = "org.jetbrains.kotlin.jvm") + + testDb("h2") { + withContainer = false + dialects("H2", "H2_MYSQL", "H2_PSQL", "H2_MARIADB", "H2_ORACLE", "H2_SQLSERVER") + + dependencies { + dependency("com.h2database:h2:${Versions.h2_v2}") + } + } + + testDb("h2_v1") { + withContainer = false + dialects("H2", "H2_MYSQL") + + dependencies { + dependency("com.h2database:h2:${Versions.h2}") + } + } + + testDb("sqlite") { + withContainer = false + dialects("sqlite") + + dependencies { + dependency("org.xerial:sqlite-jdbc:${Versions.sqlLite3}") + } + } + + testDb("mysql") { + port = 3001 + dialects("mysql") + dependencies { + dependency("mysql:mysql-connector-java:${Versions.mysql51}") + } + } + + testDb("mysql8") { + port = 3002 + dialects("mysql") + dependencies { + dependency("mysql:mysql-connector-java:${Versions.mysql80}") + } + } + + testDb("mariadb_v2") { + dialects("mariadb") + container = "mariadb" + port = 3000 + dependencies { + dependency("org.mariadb.jdbc:mariadb-java-client:${Versions.mariaDB_v2}") + } + } + + testDb("mariadb_v3") { + dialects("mariadb") + container = "mariadb" + port = 3000 + dependencies { + dependency("org.mariadb.jdbc:mariadb-java-client:${Versions.mariaDB_v3}") + } + } + + testDb("oracle") { + port = 3003 + colima = true + dialects("oracle") + dependencies { + dependency("com.oracle.database.jdbc:ojdbc8:${Versions.oracle12}") + } + } + + testDb("postgres") { + port = 3004 + dialects("postgresql") + dependencies { + dependency("org.postgresql:postgresql:${Versions.postgre}") + } + } + + testDb("postgresNG") { + port = 3004 + dialects("postgresqlng") + container = "postgres" + dependencies { + dependency("org.postgresql:postgresql:${Versions.postgre}") + dependency("com.impossibl.pgjdbc-ng:pgjdbc-ng:${Versions.postgreNG}") + } + } + + testDb("sqlserver") { + port = 3005 + dialects("sqlserver") + dependencies { + dependency("com.microsoft.sqlserver:mssql-jdbc:${Versions.sqlserver}") + } + } } diff --git a/buildScripts/docker/docker-compose-mariadb.yml b/buildScripts/docker/docker-compose-mariadb.yml index b884da7aa3..6cb742943c 100644 --- a/buildScripts/docker/docker-compose-mariadb.yml +++ b/buildScripts/docker/docker-compose-mariadb.yml @@ -1,11 +1,11 @@ version: '3.1' services: - mariadb: - image: mariadb - restart: always - ports: - - "3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' - MYSQL_DATABASE: 'testdb' + mariadb: + image: mariadb + restart: always + ports: + - "3000:3306" + environment: + MYSQL_ROOT_PASSWORD: "Exposed_password_1!" + MYSQL_DATABASE: "testdb" diff --git a/buildScripts/docker/docker-compose-mysql.yml b/buildScripts/docker/docker-compose-mysql.yml index e418594507..6eac6034ff 100644 --- a/buildScripts/docker/docker-compose-mysql.yml +++ b/buildScripts/docker/docker-compose-mysql.yml @@ -1,12 +1,12 @@ -version: '3.1' +version: "3.1" services: - mysql: - platform: linux/x86_64 - image: mysql:5.7 - restart: always - ports: - - "3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' - MYSQL_DATABASE: 'testdb' + mysql: + platform: linux/x86_64 + restart: always + image: mysql:5.7 + ports: + - "3001:3306" + environment: + MYSQL_ROOT_PASSWORD: "Exposed_password_1!" + MYSQL_DATABASE: "testdb" diff --git a/buildScripts/docker/docker-compose-mysql8.yml b/buildScripts/docker/docker-compose-mysql8.yml index 0c912b9254..06ef4602f1 100644 --- a/buildScripts/docker/docker-compose-mysql8.yml +++ b/buildScripts/docker/docker-compose-mysql8.yml @@ -1,11 +1,11 @@ version: '3.1' services: - mysql8: - image: mysql:8.0 - restart: always - ports: - - "3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' - MYSQL_DATABASE: 'testdb' + mysql8: + image: mysql:8.0 + restart: always + ports: + - "3002:3306" + environment: + MYSQL_ROOT_PASSWORD: "Exposed_password_1!" + MYSQL_DATABASE: "testdb" diff --git a/buildScripts/docker/docker-compose-oracle.yml b/buildScripts/docker/docker-compose-oracle.yml index 79eec9e9d8..4680e623fa 100644 --- a/buildScripts/docker/docker-compose-oracle.yml +++ b/buildScripts/docker/docker-compose-oracle.yml @@ -1,13 +1,11 @@ version: '3.1' services: - oracle: - container_name: oracleDB - image: quillbuilduser/oracle-18-xe:latest - ports: - - "1521" - environment: - WEB_CONSOLE: "true" - DBCA_TOTAL_MEMORY: 1024 - PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - USE_UTF8_IF_CHARSET_EMPTY: "true" \ No newline at end of file + oracle: + container_name: oracleDB + restart: always + image: gvenzl/oracle-xe:18-slim-faststart + ports: + - "3003:1521" + environment: + ORACLE_PASSWORD: "Oracle18" diff --git a/buildScripts/docker/docker-compose-postgres.yml b/buildScripts/docker/docker-compose-postgres.yml new file mode 100644 index 0000000000..ab39ca0f35 --- /dev/null +++ b/buildScripts/docker/docker-compose-postgres.yml @@ -0,0 +1,11 @@ +version: '3.1' + +services: + postgres: + image: postgres + restart: always + ports: + - "3004:5432" + environment: + POSTGRES_USER: "root" + POSTGRES_PASSWORD: "Exposed_password_1!" diff --git a/buildScripts/docker/docker-compose-sqlserver.yml b/buildScripts/docker/docker-compose-sqlserver.yml index 9ef5c9898e..1ecf490e57 100644 --- a/buildScripts/docker/docker-compose-sqlserver.yml +++ b/buildScripts/docker/docker-compose-sqlserver.yml @@ -3,9 +3,10 @@ version: '3.1' services: sqlserver: container_name: SQLServer + restart: always image: mcr.microsoft.com/azure-sql-edge:1.0.7 ports: - - "1433:1433" + - "3005:1433" environment: ACCEPT_EULA: "1" - SA_PASSWORD: "yourStrong(!)Password" + SA_PASSWORD: "Exposed_password_1!" diff --git a/buildScripts/gradle/checkstyle.gradle.kts b/buildScripts/gradle/checkstyle.gradle.kts deleted file mode 100644 index 997e95ad09..0000000000 --- a/buildScripts/gradle/checkstyle.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -import io.gitlab.arturbosch.detekt.DetektPlugin -import io.gitlab.arturbosch.detekt.extensions.DetektExtension - -apply() - -configure { - ignoreFailures = false - buildUponDefaultConfig = true - config = files( - rootDir.resolve("detekt/detekt-config.yml").takeIf { it.isFile }, - projectDir.resolve("detekt/detekt-config.yml").takeIf { it.isFile } - ) - reports { - xml.enabled = true - html.enabled = false - txt.enabled = false - sarif.enabled = false - } - parallel = true -} diff --git a/buildScripts/gradle/publishing.gradle.kts b/buildScripts/gradle/publishing.gradle.kts deleted file mode 100644 index ed3390689e..0000000000 --- a/buildScripts/gradle/publishing.gradle.kts +++ /dev/null @@ -1,41 +0,0 @@ -import org.jetbrains.exposed.gradle.* - -apply(plugin = "java-library") -apply(plugin = "maven-publish") -apply(plugin = "signing") - -_java { - withJavadocJar() - withSourcesJar() -} - -val version: String by rootProject - -_publishing { - publications { - create("exposed") { - groupId = "org.jetbrains.exposed" - artifactId = project.name - version = version - from(components["java"]) - pom { - configureMavenCentralMetadata(project) - } - signPublicationIfKeyPresent(project) - } - } - - val publishingUsername: String? = System.getenv("PUBLISHING_USERNAME") - val publishingPassword: String? = System.getenv("PUBLISHING_PASSWORD") - - repositories { - maven { - name = "Exposed" - url = uri("https://maven.pkg.jetbrains.space/public/p/exposed/release") - credentials { - username = publishingUsername - password = publishingPassword - } - } - } -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 7236698310..389d825348 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -5,9 +5,8 @@ repositories { dependencies { gradleApi() - implementation("org.jetbrains.kotlin.jvm", "org.jetbrains.kotlin.jvm.gradle.plugin", "1.9.0") - implementation("com.avast.gradle", "gradle-docker-compose-plugin", "0.14.9") - implementation("io.github.gradle-nexus", "publish-plugin", "1.0.0") + implementation("org.jetbrains.kotlin.jvm", "org.jetbrains.kotlin.jvm.gradle.plugin", "1.9.10") + implementation("com.avast.gradle", "gradle-docker-compose-plugin", "0.17.4") implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.21.0") } diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Accessors.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Accessors.kt deleted file mode 100644 index e527fac781..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Accessors.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.jetbrains.exposed.gradle - -import com.avast.gradle.dockercompose.ComposeExtension -import org.gradle.api.Project -import org.gradle.kotlin.dsl.getByName - -private inline fun Project.extByName(name: String): T = extensions.getByName(name) - -val Project.dockerCompose - get() = extByName("dockerCompose") diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/DBTestingPlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/DBTestingPlugin.kt deleted file mode 100644 index a4d70e0a31..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/DBTestingPlugin.kt +++ /dev/null @@ -1,121 +0,0 @@ -@file:Suppress("VariableNaming", "MagicNumber", "UnusedPrivateMember") - -package org.jetbrains.exposed.gradle - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.internal.tasks.testing.filter.DefaultTestFilter -import org.gradle.api.plugins.JavaPlugin -import org.gradle.api.tasks.TaskContainer -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.testing.AbstractTestTask -import org.gradle.api.tasks.testing.Test -import org.gradle.kotlin.dsl.named -import org.gradle.kotlin.dsl.register -import org.jetbrains.exposed.gradle.tasks.DBTest -import org.jetbrains.exposed.gradle.tasks.DBTestWithDockerCompose -import org.jetbrains.exposed.gradle.tasks.DBTestWithDockerCompose.Parameters - -class DBTestingPlugin : Plugin { - override fun apply(project: Project) { - project.pluginManager.apply(JavaPlugin::class.java) - project.pluginManager.apply("com.avast.gradle.docker-compose") - - with(project.tasks) { - val h2_v1 = register("h2_v1Test", "H2,H2_MYSQL") { - testRuntimeOnly("com.h2database", "h2", Versions.h2) - } - val h2_v2 = register("h2_v2Test", "H2,H2_MYSQL,H2_PSQL,H2_MARIADB,H2_ORACLE,H2_SQLSERVER") { - testRuntimeOnly("com.h2database", "h2", Versions.h2_v2) - } - val h2 = register("h2Test") { - group = "verification" - delegatedTo(h2_v1, h2_v2) - } - - val sqlite = register("sqliteTest", "SQLITE") { - testRuntimeOnly("org.xerial", "sqlite-jdbc", Versions.sqlLite3) - } - - val mysql51 = register("mysql51Test", Parameters("MYSQL", 3306)) { - testRuntimeOnly("mysql", "mysql-connector-java", Versions.mysql51) - } - val mysql80 = register("mysql80Test", Parameters("MYSQL", 3306, "mysql8")) { - testRuntimeOnly("mysql", "mysql-connector-java", Versions.mysql80) - } - val mysql = register("mysqlTest") { - group = "verification" - delegatedTo(mysql51, mysql80) - } - - val postgres = register("postgresTest", "POSTGRESQL") { - testRuntimeOnly("org.postgresql", "postgresql", Versions.postgre) - } - val postgresNG = register("postgresNGTest", "POSTGRESQLNG") { - testRuntimeOnly("org.postgresql", "postgresql", Versions.postgre) - testRuntimeOnly("com.impossibl.pgjdbc-ng", "pgjdbc-ng", Versions.postgreNG) - } - val postgresAll = register("postgresAllTest") { - group = "verification" - delegatedTo(postgres, postgresNG) - } - - val oracle = register("oracleTest", Parameters("ORACLE", 1521)) { - testRuntimeOnly("com.oracle.database.jdbc", "ojdbc8", Versions.oracle12) - } - - val sqlServer = register("sqlServerTest", Parameters("SQLSERVER", 1433)) { - testRuntimeOnly("com.microsoft.sqlserver", "mssql-jdbc", Versions.sqlserver) - } - - val mariadb_v2 = register("mariadb_v2Test", Parameters("MARIADB", 3306)) { - testRuntimeOnly("org.mariadb.jdbc", "mariadb-java-client", Versions.mariaDB_v2) - } - val mariadb_v3 = register("mariadb_v3Test", Parameters("MARIADB", 3306)) { - testRuntimeOnly("org.mariadb.jdbc", "mariadb-java-client", Versions.mariaDB_v3) - } - val mariadb = register("mariadbTest") { - group = "verification" - delegatedTo(mariadb_v2, mariadb_v3) - } - - named("test") { - delegatedTo( - h2, - sqlite, - mysql, - postgresAll, - oracle, - sqlServer, - mariadb - ) - } - } - } -} - -/** - * Defines and configure a new task, which will be created when it is required passing the given arguments to the [javax.inject.Inject]-annotated constructor. - * - * @see [TaskContainer.register] - */ -inline fun TaskContainer.register(name: String, vararg arguments: Any, noinline configuration: T.() -> Unit): TaskProvider = - register(name, T::class.java, *arguments).apply { configure { configuration() } } - -fun Test.delegatedTo(vararg tasks: TaskProvider): Test { - // don't run tests directly, delegate to other tasks - filter { - setExcludePatterns("*") - isFailOnNoMatchingTests = false - } - finalizedBy(tasks) - // Pass --tests CLI option value into delegates - doFirst { - val testsFilter = (filter as DefaultTestFilter).commandLineIncludePatterns.toList() - tasks.forEach { - it.configure { setTestNameIncludePatterns(testsFilter) } - } - } - return this -} diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Detekt.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Detekt.kt new file mode 100644 index 0000000000..adb6f25624 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Detekt.kt @@ -0,0 +1,29 @@ +package org.jetbrains.exposed.gradle + +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure + +fun Project.configureDetekt() { + apply() + + configure { + ignoreFailures = false + buildUponDefaultConfig = true + config = files( + rootDir.resolve("detekt/detekt-config.yml").takeIf { + it.isFile + }, + projectDir.resolve("detekt/detekt-config.yml").takeIf { it.isFile } + ) + reports { + xml.enabled = true + html.enabled = false + txt.enabled = false + sarif.enabled = false + } + parallel = true + } +} diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Publishing.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Publishing.kt index 7e4ee7b6fe..1b784e7ad4 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Publishing.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Publishing.kt @@ -6,6 +6,10 @@ import org.gradle.api.provider.Property import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPom import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.provideDelegate import org.gradle.plugins.signing.SigningExtension infix fun Property.by(value: T) { @@ -53,12 +57,54 @@ fun MavenPublication.signPublicationIfKeyPresent(project: Project) { } } -@Suppress("FunctionNaming") -fun Project._publishing(configure: PublishingExtension.() -> Unit) { +fun Project.publishing(configure: PublishingExtension.() -> Unit) { extensions.configure("publishing", configure) } -@Suppress("FunctionNaming") -fun Project._java(configure: JavaPluginExtension.() -> Unit) { +fun Project.java(configure: JavaPluginExtension.() -> Unit) { extensions.configure("java", configure) } + +fun Project.configurePublishing() { + apply(plugin = "java-library") + apply(plugin = "maven-publish") + apply(plugin = "signing") + + java { + withJavadocJar() + withSourcesJar() + } + + val version: String by rootProject + + publishing { + publications { + create("exposed") { + groupId = "org.jetbrains.exposed" + artifactId = project.name + + setVersion(version) + + from(components["java"]) + pom { + configureMavenCentralMetadata(project) + } + signPublicationIfKeyPresent(project) + } + } + + val publishingUsername: String? = System.getenv("PUBLISHING_USERNAME") + val publishingPassword: String? = System.getenv("PUBLISHING_PASSWORD") + + repositories { + maven { + name = "Exposed" + url = uri("https://maven.pkg.jetbrains.space/public/p/exposed/release") + credentials { + username = publishingUsername + password = publishingPassword + } + } + } + } +} diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/TestDbDsl.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/TestDbDsl.kt new file mode 100644 index 0000000000..ae1e7090cb --- /dev/null +++ b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/TestDbDsl.kt @@ -0,0 +1,124 @@ +package org.jetbrains.exposed.gradle + +import com.avast.gradle.dockercompose.ComposeExtension +import org.gradle.api.Project +import org.gradle.api.internal.tasks.testing.filter.DefaultTestFilter +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.testing.AbstractTestTask +import org.gradle.api.tasks.testing.Test +import org.gradle.configurationcache.extensions.capitalized +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register +import java.time.Duration + +const val HEALTH_TIMEOUT: Long = 60 + +class TestDb(val name: String) { + internal val dialects = mutableListOf() + var port: Int? = null + var container: String = name + var withContainer: Boolean = true + var colima: Boolean = false + + internal val dependencies = mutableListOf() + + inner class DependencyBlock { + fun dependency(dependencyNotation: String) { + dependencies.add(dependencyNotation) + } + } + + fun dependencies(block: DependencyBlock.() -> Unit) { + DependencyBlock().apply(block) + } + + fun dialects(vararg dialects: String) { + this.dialects.addAll(dialects) + } +} + +fun Project.testDb(name: String, block: TestDb.() -> Unit) { + val db = TestDb(name).apply(block) + if (db.withContainer) { + configureCompose(db) + } + + val testTask = tasks.register("test${db.name.capitalized()}") { + description = "Runs tests using ${db.name} database" + group = "verification" + systemProperties["exposed.test.name"] = db.name + systemProperties["exposed.test.container"] = if (db.withContainer) db.container else "none" + systemProperties["exposed.test.dialects"] = db.dialects.joinToString(",") { it.toUpperCase() } + outputs.cacheIf { false } + + if (!db.withContainer) return@register + dependsOn(rootProject.tasks.getByName("${db.container}ComposeUp")) + } + + dependencies { + db.dependencies.forEach { + add("testRuntimeOnly", it) + } + } + + tasks.named("test") { + delegatedTo(testTask) + } +} + +private fun Project.configureCompose(db: TestDb) { + if (rootProject.tasks.findByPath("${db.container}ComposeUp") != null) return + + rootProject.extensions.configure("dockerCompose") { + nested(db.container).apply { + environment.put("SERVICES_HOST", "127.0.0.1") + environment.put("COMPOSE_CONVERT_WINDOWS_PATHS", true) + + val isArm = System.getProperty("os.arch") == "aarch64" + if (isArm && db.colima) { + val home = System.getProperty("user.home") + val dockerHost = "unix://$home/.colima/default/docker.sock" + environment.put("DOCKER_HOST", dockerHost) + } + + useComposeFiles.set(listOf("buildScripts/docker/docker-compose-${db.container}.yml")) + removeVolumes.set(true) + stopContainers.set(false) + + waitForHealthyStateTimeout.set(Duration.ofMinutes(HEALTH_TIMEOUT)) + } + } + + val startDb = rootProject.tasks.getByName("${db.container}ComposeUp") + val stopDb = rootProject.tasks.getByName("${db.container}ComposeDownForced") + + val startCompose = rootProject.tasks.findByName("startCompose") ?: rootProject.tasks.create("startCompose") + val stopCompose = rootProject.tasks.findByName("stopCompose") ?: rootProject.tasks.create("stopCompose") + + startCompose.dependsOn(startDb) + stopCompose.dependsOn(stopDb) +} + +/** + * Delegates the execution of tests to other tasks. + * + * @param tasks The tasks to delegate the test execution to. + * @return The modified Test object. + */ +fun Test.delegatedTo(vararg tasks: TaskProvider): Test { + // don't run tests directly, delegate to other tasks + filter { + setExcludePatterns("*") + isFailOnNoMatchingTests = false + } + finalizedBy(tasks) + // Pass --tests CLI option value into delegates + doFirst { + val testsFilter = (filter as DefaultTestFilter).commandLineIncludePatterns.toList() + tasks.forEach { + it.configure { setTestNameIncludePatterns(testsFilter) } + } + } + return this +} diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Versions.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Versions.kt index 24b09e7739..bf8db43fec 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Versions.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Versions.kt @@ -1,30 +1,26 @@ package org.jetbrains.exposed.gradle object Versions { - const val kotlin = "1.7.21" - const val kotlinCoroutines = "1.6.4" + const val kotlinCoroutines = "1.7.3" const val kotlinxSerialization = "1.5.1" const val slf4j = "1.7.36" - const val log4j2 = "2.17.2" + const val log4j2 = "2.20.0" /** JDBC drivers **/ - const val h2 = "1.4.199" - const val h2_v2 = "2.1.214" - const val mariaDB_v2 = "2.7.6" - const val mariaDB_v3 = "3.0.6" + const val h2 = "1.4.200" + const val h2_v2 = "2.2.220" + const val mariaDB_v2 = "2.7.9" + const val mariaDB_v3 = "3.1.4" const val mysql51 = "5.1.49" const val mysql80 = "8.0.30" const val oracle12 = "12.2.0.1" - const val postgre = "42.4.0" + const val postgre = "42.6.0" const val postgreNG = "0.8.9" - const val sqlLite3 = "3.36.0.3" + const val sqlLite3 = "3.42.0.0" const val sqlserver = "9.4.1.jre8" /** Spring **/ - const val springFramework = "5.3.22" - const val springBoot = "2.7.2" - - /** Test Dependencies **/ - const val testContainers = "1.17.3" + const val springFramework = "5.3.29" + const val springBoot = "2.7.14" } diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTest.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTest.kt deleted file mode 100644 index 139ce1717a..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.jetbrains.exposed.gradle.tasks - -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.SourceSet -import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.testing.Test -import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.getByName -import javax.inject.Inject - -open class DBTest @Inject constructor(@get:Input val dialect: String) : Test() { - init { - group = "verification" - val projectSourceSets = project.extensions.getByName("sourceSets") - val projectTestSourceSet = projectSourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME) - val projectMainSourceSet = projectSourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) - testClassesDirs = projectTestSourceSet.output.classesDirs - classpath = projectMainSourceSet.output + projectTestSourceSet.output + projectTestSourceSet.runtimeClasspath - } - - @TaskAction - override fun executeTests() { - withSystemProperties("exposed.test.dialects" to dialect) { - super.executeTests() - } - } - - protected fun withSystemProperties(vararg sysProp: Pair, action: DBTest.() -> Unit) { - val prevValues = sysProp.associate { (name, _) -> name to systemProperties[name] } - sysProp.forEach { (name, value) -> systemProperty(name, value) } - action() - prevValues.forEach { (name, value) -> systemProperty(name, value ?: "") } - } - - fun testRuntimeOnly(group: String, name: String, version: String) { - classpath += dependencyAsConfiguration(group, name, version) - } - - private fun dependencyAsConfiguration(group: String, name: String, version: String) = - project.configurations.maybeCreate("${group}_${name}_$version").apply { - isVisible = false - defaultDependencies { - add(project.dependencies.create(group, name).apply { version { strictly(version) } }) - } - } -} diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTestWithDockerCompose.kt b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTestWithDockerCompose.kt deleted file mode 100644 index 92975a66b6..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTestWithDockerCompose.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.jetbrains.exposed.gradle.tasks - -import com.avast.gradle.dockercompose.ComposeSettings -import org.gradle.api.tasks.Input -import org.jetbrains.exposed.gradle.dockerCompose -import java.io.File -import java.time.Duration -import javax.inject.Inject - -open class DBTestWithDockerCompose(dialect: String, @get:Input val port: Int, @get:Input val dockerComposeServiceName: String) : DBTest(dialect) { - // Gradle doesn't support injection into constructors with optional parameters (as well as several constructors marked with @Inject) - // Also IDE's inline hints are abused if parameters are passed as vararg - // Workaround is to wrap all parameters into a data class and pass it into constructor - data class Parameters(val dialect: String, val port: Int, val dockerComposeServiceName: String = dialect.toLowerCase()) - - @Inject - constructor(parameters: Parameters) : this(parameters.dialect, parameters.port, parameters.dockerComposeServiceName) - - private val dockerCompose: ComposeSettings = project.dockerCompose.nested(dockerComposeServiceName).apply { - environment.put("COMPOSE_CONVERT_WINDOWS_PATHS", true) - useComposeFiles.add( - File(project.rootProject.projectDir, "buildScripts/docker/docker-compose-$dockerComposeServiceName.yml").absolutePath - ) - captureContainersOutput.set(true) - removeVolumes.set(true) - - @Suppress("MagicNumber") - waitForHealthyStateTimeout.set(Duration.ofMinutes(60)) - } - - override fun executeTests() { - with(dockerCompose) { - try { - upTask.get().up() - exposeAsEnvironment(this@DBTestWithDockerCompose) - exposeAsSystemProperties(this@DBTestWithDockerCompose) - val containerInfo = servicesInfos[dockerComposeServiceName]!! - withSystemProperties( - "exposed.test.$dockerComposeServiceName.host" to containerInfo.host, - "exposed.test.$dockerComposeServiceName.port" to (containerInfo.ports[port] ?: -1) - ) { - super.executeTests() - } - } finally { - downTask.get().down() - } - } - } -} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 82e4c5f897..ecb19d81d7 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -13,6 +13,30 @@ This project and the corresponding community is governed by the [JetBrains Open Source and Community Code of Conduct](https://confluence.jetbrains.com/display/ALL/JetBrains+Open+Source+and+Community+Code+of+Conduct). Independently of how you'd like to contribute, please make sure you read and comply with it. +## Setup + +### Testing on Apple Silicon +To run Oracle XE tests, you need to install [Colima](https://github.com/abiosoft/colima) container runtime. It will work in pair with your docker installation. +```shell +brew install colima +``` + +After installing, you need to start the colima daemon in arch x86_64 mode: +```shel +colima start --arch x86_64 --memory 4 --network-address +``` + +The test task can automatically use colima context when needed, and it's better to use default context for other tasks. +To switch the context to default, run: +```shell +docker context use default +``` + +Make sure that default is used as default docker context: +```shell +docker context list +``` + ### Code #### Pull Requests diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 6567dd054b..ed6910515b 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -549,6 +549,7 @@ public final class org/jetbrains/exposed/sql/Database { public final fun getVersion ()Ljava/math/BigDecimal; public final fun isVersionCovers (Ljava/math/BigDecimal;)Z public final fun setUseNestedTransactions (Z)V + public fun toString ()Ljava/lang/String; } public final class org/jetbrains/exposed/sql/Database$Companion { @@ -1814,6 +1815,8 @@ public final class org/jetbrains/exposed/sql/SchemaUtils { public static synthetic fun dropSchema$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Schema;ZZILjava/lang/Object;)V public final fun dropSequence ([Lorg/jetbrains/exposed/sql/Sequence;Z)V public static synthetic fun dropSequence$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Sequence;ZILjava/lang/Object;)V + public final fun listDatabases ()Ljava/util/List; + public final fun listTables ()Ljava/util/List; public final fun setSchema (Lorg/jetbrains/exposed/sql/Schema;Z)V public static synthetic fun setSchema$default (Lorg/jetbrains/exposed/sql/SchemaUtils;Lorg/jetbrains/exposed/sql/Schema;ZILjava/lang/Object;)V public final fun sortTablesByReferences (Ljava/lang/Iterable;)Ljava/util/List; @@ -2889,7 +2892,9 @@ public final class org/jetbrains/exposed/sql/statements/StatementType : java/lan public static final field GRANT Lorg/jetbrains/exposed/sql/statements/StatementType; public static final field INSERT Lorg/jetbrains/exposed/sql/statements/StatementType; public static final field OTHER Lorg/jetbrains/exposed/sql/statements/StatementType; + public static final field PRAGMA Lorg/jetbrains/exposed/sql/statements/StatementType; public static final field SELECT Lorg/jetbrains/exposed/sql/statements/StatementType; + public static final field SHOW Lorg/jetbrains/exposed/sql/statements/StatementType; public static final field TRUNCATE Lorg/jetbrains/exposed/sql/statements/StatementType; public static final field UPDATE Lorg/jetbrains/exposed/sql/statements/StatementType; public final fun getGroup ()Lorg/jetbrains/exposed/sql/statements/StatementGroup; @@ -3067,6 +3072,7 @@ public final class org/jetbrains/exposed/sql/transactions/ThreadLocalTransaction public fun setDefaultMinRepetitionDelay (J)V public fun setDefaultReadOnly (Z)V public fun setDefaultRepetitionAttempts (I)V + public fun toString ()Ljava/lang/String; } public final class org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManagerKt { @@ -3245,6 +3251,7 @@ public abstract interface class org/jetbrains/exposed/sql/vendors/DatabaseDialec public abstract fun getSupportsTernaryAffectedRowValues ()Z public abstract fun getSupportsWindowFrameGroupsMode ()Z public abstract fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + public abstract fun listDatabases ()Ljava/lang/String; public abstract fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; public abstract fun resetCaches ()V public abstract fun resetSchemaCaches ()V @@ -3284,11 +3291,12 @@ public final class org/jetbrains/exposed/sql/vendors/DatabaseDialect$DefaultImpl public static fun getSupportsTernaryAffectedRowValues (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z public static fun getSupportsWindowFrameGroupsMode (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z public static fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Expression;)Z + public static fun listDatabases (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Ljava/lang/String; public static fun setSchema (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; public static fun tableColumns (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;[Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; } -public final class org/jetbrains/exposed/sql/vendors/DefaultKt { +public final class org/jetbrains/exposed/sql/vendors/DatabaseDialectKt { public static final fun getCurrentDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; } @@ -3458,7 +3466,9 @@ public class org/jetbrains/exposed/sql/vendors/H2Dialect : org/jetbrains/exposed public fun getSupportsWindowFrameGroupsMode ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z public final fun isSecondVersion ()Z + public fun listDatabases ()Ljava/lang/String; public fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; + public fun toString ()Ljava/lang/String; } public final class org/jetbrains/exposed/sql/vendors/H2Dialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { @@ -3511,6 +3521,7 @@ public class org/jetbrains/exposed/sql/vendors/MysqlDialect : org/jetbrains/expo public final fun isFractionDateTimeSupported ()Z public final fun isTimeZoneOffsetSupported ()Z public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public fun tableExists (Lorg/jetbrains/exposed/sql/Table;)Z } public final class org/jetbrains/exposed/sql/vendors/MysqlDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { @@ -3533,6 +3544,7 @@ public class org/jetbrains/exposed/sql/vendors/OracleDialect : org/jetbrains/exp public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z public fun getSupportsOrderByNullsFirstLast ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + public fun listDatabases ()Ljava/lang/String; public fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; } @@ -3551,6 +3563,7 @@ public class org/jetbrains/exposed/sql/vendors/PostgreSQLDialect : org/jetbrains public fun getSupportsOrderByNullsFirstLast ()Z public fun getSupportsWindowFrameGroupsMode ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + public fun listDatabases ()Ljava/lang/String; public fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; } @@ -3597,6 +3610,7 @@ public class org/jetbrains/exposed/sql/vendors/SQLServerDialect : org/jetbrains/ public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z public fun getSupportsSequenceAsGeneratedKeys ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + public fun listDatabases ()Ljava/lang/String; public fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; } @@ -3616,6 +3630,7 @@ public class org/jetbrains/exposed/sql/vendors/SQLiteDialect : org/jetbrains/exp public fun getSupportsMultipleGeneratedKeys ()Z public fun getSupportsWindowFrameGroupsMode ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + public fun listDatabases ()Ljava/lang/String; } public final class org/jetbrains/exposed/sql/vendors/SQLiteDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { @@ -3640,6 +3655,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/VendorDialect : org/jetb public fun existingPrimaryKeys ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; protected fun fillConstraintCacheForTables (Ljava/util/List;)V public final fun filterCondition (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + protected final fun getAllTableNamesCache ()Ljava/util/Map; public final fun getAllTablesNames ()Ljava/util/List; protected final fun getColumnConstraintsCache ()Ljava/util/Map; public fun getDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; @@ -3664,6 +3680,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/VendorDialect : org/jetb public fun getSupportsTernaryAffectedRowValues ()Z public fun getSupportsWindowFrameGroupsMode ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + public fun listDatabases ()Ljava/lang/String; public fun modifyColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; protected final fun quoteIdentifierWhenWrongCaseOrNecessary (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; public fun resetCaches ()V diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt index 2b2853098b..1872d9fcae 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt @@ -15,6 +15,7 @@ import java.nio.ByteBuffer import java.sql.Blob import java.sql.Clob import java.sql.ResultSet +import java.sql.SQLException import java.util.* import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf @@ -467,7 +468,7 @@ class DecimalColumnType( is BigDecimal -> value is Double -> { if (value.isNaN()) { - error("Unexpected value of type Double: NaN of ${value::class.qualifiedName}") + throw SQLException("Unexpected value of type Double: NaN of ${value::class.qualifiedName}") } else { value.toBigDecimal() } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt index 7fd562c959..4e4979d7e1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt @@ -25,6 +25,9 @@ class Database private constructor( @TestOnly set + override fun toString(): String = + "ExposedDatabase[${hashCode()}]($resolvedVendor${config.explicitDialect?.let { ", dialect=$it" } ?: ""})" + internal fun metadata(body: ExposedDatabaseMetadata.() -> T): T { val transaction = TransactionManager.currentOrNull() return if (transaction == null) { @@ -52,7 +55,9 @@ class Database private constructor( fun isVersionCovers(version: BigDecimal) = this.version >= version - val supportsAlterTableWithAddColumn by lazy(LazyThreadSafetyMode.NONE) { metadata { supportsAlterTableWithAddColumn } } + val supportsAlterTableWithAddColumn by lazy( + LazyThreadSafetyMode.NONE + ) { metadata { supportsAlterTableWithAddColumn } } val supportsMultipleResultSets by lazy(LazyThreadSafetyMode.NONE) { metadata { supportsMultipleResultSets } } val identifierManager by lazy { metadata { identifierManager } } @@ -183,7 +188,15 @@ class Database private constructor( ): Database { Class.forName(driver).getDeclaredConstructor().newInstance() val dialectName = getDialectName(url) ?: error("Can't resolve dialect for connection: $url") - return doConnect(dialectName, databaseConfig, { DriverManager.getConnection(url, user, password) }, setupConnection, manager) + return doConnect( + dialectName, + databaseConfig, + { + DriverManager.getConnection(url, user, password) + }, + setupConnection, + manager + ) } fun getDefaultIsolationLevel(db: Database): Int = diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt index a3973e8e51..7de5d70d84 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt @@ -1,4 +1,5 @@ @file:Suppress("PackageDirectoryMismatch", "InvalidPackageDeclaration") + package org.jetbrains.exposed.exceptions import org.jetbrains.exposed.sql.AbstractQuery @@ -9,7 +10,11 @@ import org.jetbrains.exposed.sql.statements.expandArgs import org.jetbrains.exposed.sql.vendors.DatabaseDialect import java.sql.SQLException -class ExposedSQLException(cause: Throwable?, val contexts: List, private val transaction: Transaction) : SQLException(cause) { +class ExposedSQLException( + cause: Throwable?, + val contexts: List, + private val transaction: Transaction +) : SQLException(cause) { fun causedByQueries(): List = contexts.map { try { if (transaction.debug) { @@ -36,7 +41,9 @@ class ExposedSQLException(cause: Throwable?, val contexts: List) = TableDepthGraph(tables).sorted() + fun checkCycle(vararg tables: Table) = TableDepthGraph(tables.toList()).hasCycle() fun createStatements(vararg tables: Table): List { @@ -118,7 +119,9 @@ object SchemaUtils { @Deprecated( "Will be removed in upcoming releases. Please use overloaded version instead", - ReplaceWith("createFKey(checkNotNull(reference.foreignKey) { \"${"$"}reference does not reference anything\" })"), + ReplaceWith( + "createFKey(checkNotNull(reference.foreignKey) { \"${"$"}reference does not reference anything\" })" + ), DeprecationLevel.HIDDEN ) fun createFKey(reference: Column<*>): List { @@ -131,9 +134,13 @@ object SchemaUtils { fun createFKey(foreignKey: ForeignKeyConstraint): List = with(foreignKey) { val allFromColumnsBelongsToTheSameTable = from.all { it.table == fromTable } - require(allFromColumnsBelongsToTheSameTable) { "not all referencing columns of $foreignKey belong to the same table" } + require( + allFromColumnsBelongsToTheSameTable + ) { "not all referencing columns of $foreignKey belong to the same table" } val allTargetColumnsBelongToTheSameTable = target.all { it.table == targetTable } - require(allTargetColumnsBelongToTheSameTable) { "not all referenced columns of $foreignKey belong to the same table" } + require( + allTargetColumnsBelongToTheSameTable + ) { "not all referenced columns of $foreignKey belong to the same table" } require(from.size == target.size) { "$foreignKey referencing columns are not in accordance with referenced" } require(deleteRule != null || updateRule != null) { "$foreignKey has no reference constraint actions" } require(target.toHashSet().size == target.size) { "not all referenced columns of $foreignKey are unique" } @@ -141,7 +148,7 @@ object SchemaUtils { return createStatement() } - fun createIndex(index: Index) = index.createStatement() + fun createIndex(index: Index): List = index.createStatement() @Suppress("NestedBlockDepth", "ComplexMethod") private fun DataTypeProvider.dbDefaultToString(column: Column<*>, exp: Expression<*>): String { @@ -154,32 +161,37 @@ object SchemaUtils { is PostgreSQLDialect -> value.toString() else -> booleanToStatementString(value) } + is String -> when { - dialect is PostgreSQLDialect -> - when (column.columnType) { - is VarCharColumnType -> "'$value'::character varying" - is TextColumnType -> "'$value'::text" - else -> processForDefaultValue(exp) - } - dialect is OracleDialect || dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> - when { - column.columnType is VarCharColumnType && value == "" -> "NULL" - column.columnType is TextColumnType && value == "" -> "NULL" - else -> value - } + dialect is PostgreSQLDialect -> when (column.columnType) { + is VarCharColumnType -> "'$value'::character varying" + is TextColumnType -> "'$value'::text" + else -> processForDefaultValue(exp) + } + + dialect is OracleDialect || dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> when { + column.columnType is VarCharColumnType && value == "" -> "NULL" + column.columnType is TextColumnType && value == "" -> "NULL" + else -> value + } + else -> value } + is Enum<*> -> when (exp.columnType) { is EnumerationNameColumnType<*> -> when (dialect) { is PostgreSQLDialect -> "'${value.name}'::character varying" else -> value.name } + else -> processForDefaultValue(exp) } + is BigDecimal -> when (dialect) { is MysqlDialect -> value.setScale((exp.columnType as DecimalColumnType).scale).toString() else -> processForDefaultValue(exp) } + else -> { if (column.columnType is JsonColumnMarker) { val processed = processForDefaultValue(exp) @@ -191,6 +203,7 @@ object SchemaUtils { processed } } + is MariaDBDialect -> processed.trim('\'') is MysqlDialect -> "_utf8mb4\\'${processed.trim('(', ')', '\'')}\\" else -> processed.trim('\'') @@ -201,12 +214,10 @@ object SchemaUtils { } } } + is Function<*> -> { var processed = processForDefaultValue(exp) - if ( - exp.columnType is IDateColumnType && - (processed.startsWith("CURRENT_TIMESTAMP") || processed == "GETDATE()") - ) { + if (exp.columnType is IDateColumnType && (processed.startsWith("CURRENT_TIMESTAMP") || processed == "GETDATE()")) { when (currentDialect) { is SQLServerDialect -> processed = "getdate" is MariaDBDialect -> processed = processed.lowercase() @@ -214,6 +225,7 @@ object SchemaUtils { } processed } + else -> processForDefaultValue(exp) } } @@ -246,26 +258,25 @@ object SchemaUtils { if (dbSupportsAlterTableWithAddColumn) { // create indexes with new columns - table.indices - .filter { index -> index.columns.any { missingTableColumns.contains(it) } } - .forEach { statements.addAll(createIndex(it)) } + table.indices.filter { index -> + index.columns.any { + missingTableColumns.contains(it) + } + }.forEach { statements.addAll(createIndex(it)) } // sync existing columns val dataTypeProvider = currentDialect.dataTypeProvider - val redoColumns = existingTableColumns - .mapValues { (col, existingCol) -> - val columnType = col.columnType - val incorrectNullability = existingCol.nullable != columnType.nullable - // Exposed doesn't support changing sequences on columns - val incorrectAutoInc = existingCol.autoIncrement != columnType.isAutoInc && - col.autoIncColumnType?.autoincSeq == null - val incorrectDefaults = existingCol.defaultDbValue != col.dbDefaultValue?.let { - dataTypeProvider.dbDefaultToString(col, it) - } - val incorrectCaseSensitiveName = existingCol.name.inProperCase() != col.nameUnquoted().inProperCase() - ColumnDiff(incorrectNullability, incorrectAutoInc, incorrectDefaults, incorrectCaseSensitiveName) + val redoColumns = existingTableColumns.mapValues { (col, existingCol) -> + val columnType = col.columnType + val incorrectNullability = existingCol.nullable != columnType.nullable + // Exposed doesn't support changing sequences on columns + val incorrectAutoInc = existingCol.autoIncrement != columnType.isAutoInc && col.autoIncColumnType?.autoincSeq == null + val incorrectDefaults = existingCol.defaultDbValue != col.dbDefaultValue?.let { + dataTypeProvider.dbDefaultToString(col, it) } - .filterValues { it.hasDifferences() } + val incorrectCaseSensitiveName = existingCol.name.inProperCase() != col.nameUnquoted().inProperCase() + ColumnDiff(incorrectNullability, incorrectAutoInc, incorrectDefaults, incorrectCaseSensitiveName) + }.filterValues { it.hasDifferences() } redoColumns.flatMapTo(statements) { (col, changedState) -> col.modifyStatements(changedState) } @@ -301,10 +312,14 @@ object SchemaUtils { for ((foreignKey, existingConstraint) in foreignKeyConstraints) { if (existingConstraint == null) { statements.addAll(createFKey(foreignKey)) - } else if (existingConstraint.targetTable != foreignKey.targetTable || - foreignKey.deleteRule != existingConstraint.deleteRule || - foreignKey.updateRule != existingConstraint.updateRule - ) { + continue + } + + val noForeignKey = existingConstraint.targetTable != foreignKey.targetTable + val deleteRuleMismatch = foreignKey.deleteRule != existingConstraint.deleteRule + val updateRuleMismatch = foreignKey.updateRule != existingConstraint.updateRule + + if (noForeignKey || deleteRuleMismatch || updateRuleMismatch) { statements.addAll(existingConstraint.dropStatement()) statements.addAll(createFKey(foreignKey)) } @@ -360,6 +375,24 @@ object SchemaUtils { } } + /** + * Returns a list of all databases. + * + * @return A list of strings representing the names of all databases. + */ + fun listDatabases(): List { + val transaction = TransactionManager.current() + return with(transaction) { + exec(currentDialect.listDatabases()) { + val result = mutableListOf() + while (it.next()) { + result.add(it.getString(1).lowercase()) + } + result + } ?: emptyList() + } + } + /** * Drops databases * @@ -425,8 +458,10 @@ object SchemaUtils { } val executedStatements = createStatements + alterStatements logTimeSpent("Checking mapping consistence", withLogs) { - val modifyTablesStatements = checkMappingConsistence(tables = tables, withLogs) - .filter { it !in executedStatements } + val modifyTablesStatements = checkMappingConsistence( + tables = tables, + withLogs + ).filter { it !in executedStatements } execStatements(inBatch, modifyTablesStatements) commit() } @@ -448,8 +483,10 @@ object SchemaUtils { } val executedStatements = createStatements + alterStatements val modifyTablesStatements = logTimeSpent("Checking mapping consistence", withLogs) { - checkMappingConsistence(tables = tablesToAlter.toTypedArray(), withLogs) - .filter { it !in executedStatements } + checkMappingConsistence( + tables = tablesToAlter.toTypedArray(), + withLogs + ).filter { it !in executedStatements } } return executedStatements + modifyTablesStatements } @@ -486,14 +523,15 @@ object SchemaUtils { } val excessiveIndices = - currentDialect.existingIndices(*tables) - .flatMap { it.value } - .groupBy { Triple(it.table, it.unique, it.columns.joinToString { it.name }) } + currentDialect.existingIndices(*tables).flatMap { + it.value + }.groupBy { Triple(it.table, it.unique, it.columns.joinToString { it.name }) } .filter { it.value.size > 1 } if (excessiveIndices.isNotEmpty()) { exposedLogger.warn("List of excessive indices:") excessiveIndices.forEach { (triple, indices) -> - exposedLogger.warn("\t\t\t'${triple.first.tableName}'.'${triple.third}' -> ${indices.joinToString(", ") { it.indexName }}") + val indexNames = indices.joinToString(", ") { it.indexName } + exposedLogger.warn("\t\t\t'${triple.first.tableName}'.'${triple.third}' -> $indexNames") } exposedLogger.info("SQL Queries to remove excessive indices:") excessiveIndices.forEach { @@ -548,8 +586,9 @@ object SchemaUtils { nameDiffers.add(mappedIndex) } - notMappedIndices.getOrPut(table.nameInDatabaseCase()) { hashSetOf() } - .addAll(existingTableIndices.subtract(mappedIndices)) + notMappedIndices.getOrPut(table.nameInDatabaseCase()) { + hashSetOf() + }.addAll(existingTableIndices.subtract(mappedIndices)) missingIndices.addAll(mappedIndices.subtract(existingTableIndices)) } @@ -585,13 +624,17 @@ object SchemaUtils { } } + /** + * Retrieves a list of all table names in the current database. + * + * @return A list of table names as strings. + */ + fun listTables(): List = currentDialect.allTablesNames() + fun drop(vararg tables: Table, inBatch: Boolean = false) { if (tables.isEmpty()) return with(TransactionManager.current()) { - var tablesForDeletion = - sortTablesByReferences(tables.toList()) - .reversed() - .filter { it in tables } + var tablesForDeletion = sortTablesByReferences(tables.toList()).reversed().filter { it in tables } if (!currentDialect.supportsIfNotExists) { tablesForDeletion = tablesForDeletion.filter { it.exists() } } @@ -619,6 +662,7 @@ object SchemaUtils { is MysqlDialect -> { connection.catalog = schema.identifier } + is H2Dialect -> { connection.schema = schema.identifier } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index 115ce24216..5503d8c44b 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -39,7 +39,9 @@ interface FieldSet { fields.forEach { if (it is CompositeColumn<*>) { unrolled.addAll(it.getRealColumns()) - } else unrolled.add(it) + } else { + unrolled.add(it) + } } return unrolled @@ -174,7 +176,9 @@ class Join( val table: ColumnSet ) : ColumnSet() { - override val columns: List> get() = joinParts.flatMapTo(table.columns.toMutableList()) { it.joinPart.columns } + override val columns: List> get() = joinParts.flatMapTo( + table.columns.toMutableList() + ) { it.joinPart.columns } internal val joinParts: MutableList = mutableListOf() @@ -245,12 +249,16 @@ class Join( val fkKeys = findKeys(this, otherTable) ?: findKeys(otherTable, this) ?: emptyList() return when { joinType != JoinType.CROSS && fkKeys.isEmpty() -> { - error("Cannot join with $otherTable as there is no matching primary key/foreign key pair and constraint missing") + error( + "Cannot join with $otherTable as there is no matching primary key/foreign key pair and constraint missing" + ) } fkKeys.any { it.second.size > 1 } -> { val references = fkKeys.joinToString(" & ") { "${it.first} -> ${it.second.joinToString()}" } - error("Cannot join with $otherTable as there is multiple primary key <-> foreign key references.\n$references") + error( + "Cannot join with $otherTable as there is multiple primary key <-> foreign key references.\n$references" + ) } else -> { @@ -286,7 +294,9 @@ class Join( val additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null ) { init { - require(joinType == JoinType.CROSS || conditions.isNotEmpty() || additionalConstraint != null) { "Missing join condition on $${this.joinPart}" } + require( + joinType == JoinType.CROSS || conditions.isNotEmpty() || additionalConstraint != null + ) { "Missing join condition on $${this.joinPart}" } } fun describe(transaction: Transaction, builder: QueryBuilder) = with(builder) { @@ -341,7 +351,9 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { internal val tableNameWithoutScheme: String get() = tableName.substringAfterLast(".") // Table name may contain quotes, remove those before appending - internal val tableNameWithoutSchemeSanitized: String get() = tableNameWithoutScheme.replace("\"", "").replace("'", "") + internal val tableNameWithoutSchemeSanitized: String get() = tableNameWithoutScheme + .replace("\"", "") + .replace("'", "") private val _columns = mutableListOf>() @@ -383,7 +395,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { tableNameWithoutScheme.inProperCase().trim('\"', '\'') } - override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { append(s.identity(this@Table)) } + override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { + append( + s.identity(this@Table) + ) + } // Join operations @@ -408,9 +424,19 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { // Column registration /** Adds a column of the specified [type] and with the specified [name] to the table. */ - fun registerColumn(name: String, type: IColumnType): Column = Column(this, name, type).also { _columns.addColumn(it) } - - fun > registerCompositeColumn(column: T): T = column.apply { getRealColumns().forEach { _columns.addColumn(it) } } + fun registerColumn(name: String, type: IColumnType): Column = Column( + this, + name, + type + ).also { _columns.addColumn(it) } + + fun > registerCompositeColumn(column: T): T = column.apply { + getRealColumns().forEach { + _columns.addColumn( + it + ) + } + } /** * Replaces the specified [oldColumn] with the specified [newColumn] in the table. @@ -431,7 +457,9 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { // Primary keys - internal fun isCustomPKNameDefined(): Boolean = primaryKey?.let { it.name != "pk_$tableNameWithoutSchemeSanitized" } == true + internal fun isCustomPKNameDefined(): Boolean = primaryKey?.let { + it.name != "pk_$tableNameWithoutSchemeSanitized" + } == true /** * Represents a primary key composed by the specified [columns], and with the specified [name]. @@ -477,7 +505,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Creates an [EntityID] column, with the specified [name], for storing the same objects as the specified [originalColumn]. */ fun > entityId(name: String, originalColumn: Column): Column> { val columnTypeCopy = originalColumn.columnType.cloneAsBaseType() - val answer = Column>(this, name, EntityIDColumnType(Column(originalColumn.table, name, columnTypeCopy))) + val answer = Column>( + this, + name, + EntityIDColumnType(Column(originalColumn.table, name, columnTypeCopy)) + ) _columns.addColumn(answer) return answer } @@ -551,7 +583,10 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @param precision Total count of significant digits in the whole number, that is, the number of digits to both sides of the decimal point. * @param scale Count of decimal digits in the fractional part. */ - fun decimal(name: String, precision: Int, scale: Int): Column = registerColumn(name, DecimalColumnType(precision, scale)) + fun decimal(name: String, precision: Int, scale: Int): Column = registerColumn( + name, + DecimalColumnType(precision, scale) + ) // Character columns @@ -562,13 +597,19 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * Creates a character column, with the specified [name], for storing strings with the specified [length] using the specified text [collate] type. * If no collate type is specified then the database default is used. */ - fun char(name: String, length: Int, collate: String? = null): Column = registerColumn(name, CharColumnType(length, collate)) + fun char(name: String, length: Int, collate: String? = null): Column = registerColumn( + name, + CharColumnType(length, collate) + ) /** * Creates a character column, with the specified [name], for storing strings with the specified maximum [length] using the specified text [collate] type. * If no collate type is specified then the database default is used. */ - fun varchar(name: String, length: Int, collate: String? = null): Column = registerColumn(name, VarCharColumnType(length, collate)) + fun varchar(name: String, length: Int, collate: String? = null): Column = registerColumn( + name, + VarCharColumnType(length, collate) + ) /** * Creates a character column, with the specified [name], for storing strings of arbitrary length using the specified [collate] type. @@ -639,7 +680,10 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { // Enumeration columns /** Creates an enumeration column, with the specified [name], for storing enums of type [klass] by their ordinal. */ - fun > enumeration(name: String, klass: KClass): Column = registerColumn(name, EnumerationColumnType(klass)) + fun > enumeration(name: String, klass: KClass): Column = registerColumn( + name, + EnumerationColumnType(klass) + ) /** Creates an enumeration column, with the specified [name], for storing enums of type [T] by their ordinal. */ inline fun > enumeration(name: String) = enumeration(name, T::class) @@ -742,7 +786,12 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @param ref A column from another table which will be used as a "parent". * @see [references] */ - infix fun , S : T, C : Column> C.references(ref: Column): C = references(ref, null, null, null) + infix fun , S : T, C : Column> C.references(ref: Column): C = references( + ref, + null, + null, + null + ) /** * Create reference from a @receiver column to [ref] column with [onDelete], [onUpdate], and [fkName] options. @@ -817,7 +866,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { onUpdate: ReferenceOption? = null, fkName: String? = null ): Column { - val column = Column(this, name, refColumn.columnType.cloneAsBaseType()).references(refColumn, onDelete, onUpdate, fkName) + val column = Column( + this, + name, + refColumn.columnType.cloneAsBaseType() + ).references(refColumn, onDelete, onUpdate, fkName) _columns.addColumn(column) return column } @@ -982,9 +1035,13 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { ) { _indices.add( Index( - columns.toList(), isUnique, customIndexName, indexType, + columns.toList(), + isUnique, + customIndexName, + indexType, filterCondition?.invoke(SqlExpressionBuilder), - functions, functions?.let { this } + functions, + functions?.let { this } ) ) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt index 462bb5068c..35fcb08b4d 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt @@ -108,8 +108,7 @@ open class Transaction( @Language("sql") stmt: String, args: Iterable> = emptyList(), explicitStatementType: StatementType? = null - ) = - exec(stmt, args, explicitStatementType) { } + ) = exec(stmt, args, explicitStatementType) { } fun exec( @Language("sql") stmt: String, @@ -126,7 +125,7 @@ open class Transaction( return exec(object : Statement(type, emptyList()) { override fun PreparedStatementApi.executeInternal(transaction: Transaction): T? { val result = when (type) { - StatementType.SELECT, StatementType.EXEC -> executeQuery() + StatementType.SELECT, StatementType.EXEC, StatementType.SHOW, StatementType.PRAGMA -> executeQuery() else -> { executeUpdate() resultSet diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/Statement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/Statement.kt index 8be75ebce7..3a92361eb4 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/Statement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/Statement.kt @@ -61,8 +61,8 @@ abstract class Statement(val type: StatementType, val targets: List() + override fun toString(): String { + return "ThreadLocalTransactionManager[${hashCode()}](db=$db)" + } + override fun newTransaction(isolation: Int, readOnly: Boolean, outerTransaction: Transaction?): Transaction { val transaction = outerTransaction?.takeIf { !db.useNestedTransactions } ?: Transaction( ThreadLocalTransaction( @@ -166,46 +171,45 @@ fun transaction( readOnly: Boolean = false, db: Database? = null, statement: Transaction.() -> T -): T = - keepAndRestoreTransactionRefAfterRun(db) { - val outer = TransactionManager.currentOrNull() +): T = keepAndRestoreTransactionRefAfterRun(db) { + val outer = TransactionManager.currentOrNull() - if (outer != null && (db == null || outer.db == db)) { - val outerManager = outer.db.transactionManager + if (outer != null && (db == null || outer.db == db)) { + val outerManager = outer.db.transactionManager - val transaction = outerManager.newTransaction(transactionIsolation, readOnly, outer) + val transaction = outerManager.newTransaction(transactionIsolation, readOnly, outer) + try { + transaction.statement().also { + if (outer.db.useNestedTransactions) { + transaction.commit() + } + } + } finally { + TransactionManager.resetCurrent(outerManager) + } + } else { + val existingForDb = db?.transactionManager + existingForDb?.currentOrNull()?.let { transaction -> + val currentManager = outer?.db.transactionManager try { + TransactionManager.resetCurrent(existingForDb) transaction.statement().also { - if (outer.db.useNestedTransactions) { + if (db.useNestedTransactions) { transaction.commit() } } } finally { - TransactionManager.resetCurrent(outerManager) + TransactionManager.resetCurrent(currentManager) } - } else { - val existingForDb = db?.transactionManager - existingForDb?.currentOrNull()?.let { transaction -> - val currentManager = outer?.db.transactionManager - try { - TransactionManager.resetCurrent(existingForDb) - transaction.statement().also { - if (db.useNestedTransactions) { - transaction.commit() - } - } - } finally { - TransactionManager.resetCurrent(currentManager) - } - } ?: inTopLevelTransaction( - transactionIsolation, - readOnly, - db, - null, - statement - ) - } + } ?: inTopLevelTransaction( + transactionIsolation, + readOnly, + db, + null, + statement + ) } +} fun inTopLevelTransaction( transactionIsolation: Int, @@ -249,6 +253,7 @@ fun inTopLevelTransaction( intermediateDelay += retryInterval * repetitions ThreadLocalRandom.current().nextLong(intermediateDelay, intermediateDelay + retryInterval) } + transaction.minRepetitionDelay == transaction.maxRepetitionDelay -> transaction.minRepetitionDelay else -> 0 } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionApi.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionApi.kt index 9d7bd393fd..2399a3902d 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionApi.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionApi.kt @@ -108,6 +108,10 @@ interface TransactionManager { private class TransactionManagerThreadLocal : ThreadLocal() { var isInitialized = false + override fun get(): TransactionManager { + return super.get() + } + override fun initialValue(): TransactionManager { isInitialized = true return defaultDatabase?.let { registeredDatabases.getValue(it) } ?: NotInitializedManager diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/ColumnMetadata.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/ColumnMetadata.kt new file mode 100644 index 0000000000..a93f17d89e --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/ColumnMetadata.kt @@ -0,0 +1,23 @@ +package org.jetbrains.exposed.sql.vendors + +/** + * Represents metadata information about a specific column. + */ +data class ColumnMetadata( + /** Name of the column. */ + val name: String, + /** + * Type of the column. + * + * @see java.sql.Types + */ + val type: Int, + /** Whether the column if nullable or not. */ + val nullable: Boolean, + /** Optional size of the column. */ + val size: Int?, + /** Is the column auto increment */ + val autoIncrement: Boolean, + /** Default value */ + val defaultDbValue: String?, +) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt new file mode 100644 index 0000000000..4c9650c67f --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DataTypeProvider.kt @@ -0,0 +1,153 @@ +package org.jetbrains.exposed.sql.vendors + +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Function +import java.nio.ByteBuffer +import java.util.* + +/** + * Provides definitions for all the supported SQL data types. + * By default, definitions from the SQL standard are provided but if a vendor doesn't support a specific type, or it is + * implemented differently, the corresponding function should be overridden. + */ +abstract class DataTypeProvider { + // Numeric types + + /** Numeric type for storing 1-byte integers. */ + open fun byteType(): String = "TINYINT" + + /** Numeric type for storing 1-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL, MariaDB, or SQL Server, this will represent the 2-byte + * integer type. + */ + open fun ubyteType(): String = "SMALLINT" + + /** Numeric type for storing 2-byte integers. */ + open fun shortType(): String = "SMALLINT" + + /** Numeric type for storing 2-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this will represent the 4-byte integer type. + */ + open fun ushortType(): String = "INT" + + /** Numeric type for storing 4-byte integers. */ + open fun integerType(): String = "INT" + + /** Numeric type for storing 4-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this will represent the 8-byte integer type. + */ + open fun uintegerType(): String = "BIGINT" + + /** Numeric type for storing 4-byte integers, marked as auto-increment. */ + open fun integerAutoincType(): String = "INT AUTO_INCREMENT" + + /** Numeric type for storing 8-byte integers. */ + open fun longType(): String = "BIGINT" + + /** Numeric type for storing 8-byte unsigned integers. */ + open fun ulongType(): String = "BIGINT" + + /** Numeric type for storing 8-byte integers, and marked as auto-increment. */ + open fun longAutoincType(): String = "BIGINT AUTO_INCREMENT" + + /** Numeric type for storing 4-byte (single precision) floating-point numbers. */ + open fun floatType(): String = "FLOAT" + + /** Numeric type for storing 8-byte (double precision) floating-point numbers. */ + open fun doubleType(): String = "DOUBLE PRECISION" + + // Character types + + /** Character type for storing strings of variable length up to a maximum. */ + open fun varcharType(colLength: Int): String = "VARCHAR($colLength)" + + /** Character type for storing strings of variable length. + * Some database (postgresql) use the same data type name to provide virtually _unlimited_ length. */ + open fun textType(): String = "TEXT" + + /** Character type for storing strings of _medium_ length. */ + open fun mediumTextType(): String = "TEXT" + + /** Character type for storing strings of variable and _large_ length. */ + open fun largeTextType(): String = "TEXT" + + // Binary data types + + /** Binary type for storing binary strings of variable and _unlimited_ length. */ + abstract fun binaryType(): String + + /** Binary type for storing binary strings of a specific [length]. */ + open fun binaryType(length: Int): String = if (length == Int.MAX_VALUE) "VARBINARY(MAX)" else "VARBINARY($length)" + + /** Binary type for storing BLOBs. */ + open fun blobType(): String = "BLOB" + + /** Binary type for storing [UUID]. */ + open fun uuidType(): String = "BINARY(16)" + + @Suppress("MagicNumber") + open fun uuidToDB(value: UUID): Any = + ByteBuffer.allocate(16).putLong(value.mostSignificantBits).putLong(value.leastSignificantBits).array() + + // Date/Time types + + /** Data type for storing both date and time without a time zone. */ + open fun dateTimeType(): String = "DATETIME" + + /** Data type for storing both date and time with a time zone. */ + open fun timestampWithTimeZoneType(): String = "TIMESTAMP WITH TIME ZONE" + + /** Time type for storing time without a time zone. */ + open fun timeType(): String = "TIME" + + /** Data type for storing date without time or a time zone. */ + open fun dateType(): String = "DATE" + + // Boolean type + + /** Data type for storing boolean values. */ + open fun booleanType(): String = "BOOLEAN" + + /** Returns the SQL representation of the specified [bool] value. */ + open fun booleanToStatementString(bool: Boolean): String = bool.toString().uppercase() + + /** Returns the boolean value of the specified SQL [value]. */ + open fun booleanFromStringToBoolean(value: String): Boolean = value.toBoolean() + + // JSON types + + /** Data type for storing JSON in a non-binary text format. */ + open fun jsonType(): String = "JSON" + + /** Data type for storing JSON in a decomposed binary format. */ + open fun jsonBType(): String = + throw UnsupportedByDialectException("This vendor does not support binary JSON data type", currentDialect) + + // Misc. + + /** Returns the SQL representation of the specified expression, for it to be used as a column default value. */ + open fun processForDefaultValue(e: Expression<*>): String = when { + e is LiteralOp<*> && e.columnType is JsonColumnMarker -> if (currentDialect is H2Dialect) { + "$e".substringAfter("JSON ") + } else { + "'$e'" + } + + e is LiteralOp<*> -> "$e" + e is Function<*> -> "$e" + currentDialect is MysqlDialect -> "$e" + currentDialect is SQLServerDialect -> "$e" + else -> "($e)" + } + + open fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) { + queryBuilder.append((expression as? ExpressionAlias<*>)?.alias ?: expression, " ", sortOrder.code) + } + + /** Returns the hex-encoded value to be inserted into the database. */ + abstract fun hexToDb(hexString: String): String +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt new file mode 100644 index 0000000000..f8da9f1772 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt @@ -0,0 +1,169 @@ +package org.jetbrains.exposed.sql.vendors + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.TransactionManager + +/** + * Common interface for all database dialects. + */ +@Suppress("TooManyFunctions") +interface DatabaseDialect { + /** Name of this dialect. */ + val name: String + + /** Data type provider of this dialect. */ + val dataTypeProvider: DataTypeProvider + + /** Function provider of this dialect. */ + val functionProvider: FunctionProvider + + /** Returns `true` if the dialect supports the `IF EXISTS`/`IF NOT EXISTS` option when creating, altering or dropping objects, `false` otherwise. */ + val supportsIfNotExists: Boolean get() = true + + /** Returns `true` if the dialect supports the creation of sequences, `false` otherwise. */ + val supportsCreateSequence: Boolean get() = true + + /** Returns `true` if the dialect requires the use of a sequence to create an auto-increment column, `false` otherwise. */ + val needsSequenceToAutoInc: Boolean get() = false + + /** Returns the default reference option for the dialect. */ + val defaultReferenceOption: ReferenceOption get() = ReferenceOption.RESTRICT + + /** Returns `true` if the dialect requires the use of quotes when using symbols in object names, `false` otherwise. */ + val needsQuotesWhenSymbolsInNames: Boolean get() = true + + /** Returns `true` if the dialect supports returning multiple generated keys as a result of an insert operation, `false` otherwise. */ + val supportsMultipleGeneratedKeys: Boolean + + /** Returns`true` if the dialect supports returning generated keys obtained from a sequence. */ + val supportsSequenceAsGeneratedKeys: Boolean get() = supportsCreateSequence + val supportsOnlyIdentifiersInGeneratedKeys: Boolean get() = false + + /** Returns `true` if the dialect supports an upsert operation returning an affected-row value of 0, 1, or 2. */ + val supportsTernaryAffectedRowValues: Boolean get() = false + + /** Returns`true` if the dialect supports schema creation. */ + val supportsCreateSchema: Boolean get() = true + + /** Returns `true` if the dialect supports subqueries within a UNION/EXCEPT/INTERSECT statement */ + val supportsSubqueryUnions: Boolean get() = false + + val supportsDualTableConcept: Boolean get() = false + + val supportsOrderByNullsFirstLast: Boolean get() = false + + /** Returns `true` if the dialect supports window function definitions with GROUPS mode in frame clause */ + val supportsWindowFrameGroupsMode: Boolean get() = false + + val likePatternSpecialChars: Map get() = defaultLikePatternSpecialChars + + /** Returns true if autoCommit should be enabled to create/drop database */ + val requiresAutoCommitOnCreateDrop: Boolean get() = false + + /** Returns the name of the current database. */ + fun getDatabase(): String + + /** Returns a list with the names of all the defined tables. */ + fun allTablesNames(): List + + /** Checks if the specified table exists in the database. */ + fun tableExists(table: Table): Boolean + + /** Checks if the specified schema exists. */ + fun schemaExists(schema: Schema): Boolean + + fun checkTableMapping(table: Table): Boolean = true + + /** Returns a map with the column metadata of all the defined columns in each of the specified [tables]. */ + fun tableColumns(vararg tables: Table): Map> = emptyMap() + + /** Returns a map with the foreign key constraints of all the defined columns sets in each of the specified [tables]. */ + fun columnConstraints( + vararg tables: Table + ): Map>>, List> = emptyMap() + + /** Returns a map with all the defined indices in each of the specified [tables]. */ + fun existingIndices(vararg tables: Table): Map> = emptyMap() + + /** Returns a map with the primary key metadata in each of the specified [tables]. */ + fun existingPrimaryKeys(vararg tables: Table): Map = emptyMap() + + /** Returns `true` if the dialect supports `SELECT FOR UPDATE` statements, `false` otherwise. */ + fun supportsSelectForUpdate(): Boolean + + /** Returns `true` if the specified [e] is allowed as a default column value in the dialect, `false` otherwise. */ + fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = e is LiteralOp<*> + + /** Returns the catalog name of the connection of the specified [transaction]. */ + fun catalog(transaction: Transaction): String = transaction.connection.catalog + + /** Clears any cached values. */ + fun resetCaches() + + /** Clears any cached values including schema names. */ + fun resetSchemaCaches() + + // Specific SQL statements + + /** Returns the SQL command that creates the specified [index]. */ + fun createIndex(index: Index): String + + /** Returns the SQL command that drops the specified [indexName] from the specified [tableName]. */ + fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String + + /** Returns the SQL command that modifies the specified [column]. */ + fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List + + /** Returns the SQL command that adds a primary key specified [pkName] to an existing [table]. */ + fun addPrimaryKey(table: Table, pkName: String?, vararg pkColumns: Column<*>): String + + fun createDatabase(name: String) = "CREATE DATABASE IF NOT EXISTS ${name.inProperCase()}" + + fun listDatabases(): String = "SHOW DATABASES" + + fun dropDatabase(name: String) = "DROP DATABASE IF EXISTS ${name.inProperCase()}" + + fun setSchema(schema: Schema): String = "SET SCHEMA ${schema.identifier}" + + fun createSchema(schema: Schema): String = buildString { + append("CREATE SCHEMA IF NOT EXISTS ") + append(schema.identifier) + appendIfNotNull(" AUTHORIZATION ", schema.authorization) + } + + fun dropSchema(schema: Schema, cascade: Boolean): String = buildString { + append("DROP SCHEMA IF EXISTS ", schema.identifier) + + if (cascade) { + append(" CASCADE") + } + } + + companion object { + private val defaultLikePatternSpecialChars = mapOf('%' to null, '_' to null) + } +} + +private val explicitDialect = ThreadLocal() + +internal fun withDialect(dialect: DatabaseDialect, body: () -> T): T { + return try { + explicitDialect.set(dialect) + body() + } finally { + explicitDialect.set(null) + } +} + +/** Returns the dialect used in the current transaction, may throw an exception if there is no current transaction. */ +val currentDialect: DatabaseDialect get() = explicitDialect.get() ?: TransactionManager.current().db.dialect + +internal val currentDialectIfAvailable: DatabaseDialect? + get() = if (TransactionManager.isInitialized() && TransactionManager.currentOrNull() != null) { + currentDialect + } else { + null + } + +internal fun String.inProperCase(): String = + TransactionManager.currentOrNull()?.db?.identifierManager?.inProperCase(this@inProperCase) ?: this diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/ForUpdateOption.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/ForUpdateOption.kt new file mode 100644 index 0000000000..0db916af7b --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/ForUpdateOption.kt @@ -0,0 +1,83 @@ +package org.jetbrains.exposed.sql.vendors + +import org.jetbrains.exposed.sql.Table + +sealed class ForUpdateOption(open val querySuffix: String) { + + internal object NoForUpdateOption : ForUpdateOption("") { + override val querySuffix: String get() = error("querySuffix should not be called for NoForUpdateOption object") + } + + object ForUpdate : ForUpdateOption("FOR UPDATE") + + // https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html for clarification + object MySQL { + object ForShare : ForUpdateOption("FOR SHARE") + + object LockInShareMode : ForUpdateOption("LOCK IN SHARE MODE") + } + + // https://mariadb.com/kb/en/select/#lock-in-share-modefor-update + object MariaDB { + object LockInShareMode : ForUpdateOption("LOCK IN SHARE MODE") + } + + // https://www.postgresql.org/docs/current/sql-select.html + // https://www.postgresql.org/docs/12/explicit-locking.html#LOCKING-ROWS for clarification + object PostgreSQL { + enum class MODE(val statement: String) { + NO_WAIT("NOWAIT"), SKIP_LOCKED("SKIP LOCKED") + } + + abstract class ForUpdateBase( + querySuffix: String, + private val mode: MODE? = null, + private vararg val ofTables: Table + ) : ForUpdateOption("") { + private val preparedQuerySuffix = buildString { + append(querySuffix) + ofTables.takeIf { it.isNotEmpty() }?.let { tables -> + append(" OF ") + tables.joinTo(this, separator = ",") { it.tableName } + } + mode?.let { + append(" ${it.statement}") + } + } + final override val querySuffix: String = preparedQuerySuffix + } + + class ForUpdate( + mode: MODE? = null, + vararg ofTables: Table + ) : ForUpdateBase("FOR UPDATE", mode, ofTables = ofTables) + + open class ForNoKeyUpdate( + mode: MODE? = null, + vararg ofTables: Table + ) : ForUpdateBase("FOR NO KEY UPDATE", mode, ofTables = ofTables) { + companion object : ForNoKeyUpdate() + } + + open class ForShare( + mode: MODE? = null, + vararg ofTables: Table + ) : ForUpdateBase("FOR SHARE", mode, ofTables = ofTables) { + companion object : ForShare() + } + + open class ForKeyShare( + mode: MODE? = null, + vararg ofTables: Table + ) : ForUpdateBase("FOR KEY SHARE", mode, ofTables = ofTables) { + companion object : ForKeyShare() + } + } + + // https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2066346 + object Oracle { + object ForUpdateNoWait : ForUpdateOption("FOR UPDATE NOWAIT") + + class ForUpdateWait(timeout: Int) : ForUpdateOption("FOR UPDATE WAIT $timeout") + } +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt similarity index 50% rename from exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt rename to exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt index d67bb8ed53..f5595d06a6 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt @@ -3,156 +3,6 @@ package org.jetbrains.exposed.sql.vendors import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.exceptions.throwUnsupportedException import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.Function -import org.jetbrains.exposed.sql.transactions.TransactionManager -import java.nio.ByteBuffer -import java.util.* -import java.util.concurrent.ConcurrentHashMap - -/** - * Provides definitions for all the supported SQL data types. - * By default, definitions from the SQL standard are provided but if a vendor doesn't support a specific type, or it is - * implemented differently, the corresponding function should be overridden. - */ -abstract class DataTypeProvider { - // Numeric types - - /** Numeric type for storing 1-byte integers. */ - open fun byteType(): String = "TINYINT" - - /** Numeric type for storing 1-byte unsigned integers. - * - * **Note:** If the database being used is not MySQL, MariaDB, or SQL Server, this will represent the 2-byte - * integer type. - */ - open fun ubyteType(): String = "SMALLINT" - - /** Numeric type for storing 2-byte integers. */ - open fun shortType(): String = "SMALLINT" - - /** Numeric type for storing 2-byte unsigned integers. - * - * **Note:** If the database being used is not MySQL or MariaDB, this will represent the 4-byte integer type. - */ - open fun ushortType(): String = "INT" - - /** Numeric type for storing 4-byte integers. */ - open fun integerType(): String = "INT" - - /** Numeric type for storing 4-byte unsigned integers. - * - * **Note:** If the database being used is not MySQL or MariaDB, this will represent the 8-byte integer type. - */ - open fun uintegerType(): String = "BIGINT" - - /** Numeric type for storing 4-byte integers, marked as auto-increment. */ - open fun integerAutoincType(): String = "INT AUTO_INCREMENT" - - /** Numeric type for storing 8-byte integers. */ - open fun longType(): String = "BIGINT" - - /** Numeric type for storing 8-byte unsigned integers. */ - open fun ulongType(): String = "BIGINT" - - /** Numeric type for storing 8-byte integers, and marked as auto-increment. */ - open fun longAutoincType(): String = "BIGINT AUTO_INCREMENT" - - /** Numeric type for storing 4-byte (single precision) floating-point numbers. */ - open fun floatType(): String = "FLOAT" - - /** Numeric type for storing 8-byte (double precision) floating-point numbers. */ - open fun doubleType(): String = "DOUBLE PRECISION" - - // Character types - - /** Character type for storing strings of variable length up to a maximum. */ - open fun varcharType(colLength: Int): String = "VARCHAR($colLength)" - - /** Character type for storing strings of variable length. - * Some database (postgresql) use the same data type name to provide virtually _unlimited_ length. */ - open fun textType(): String = "TEXT" - - /** Character type for storing strings of _medium_ length. */ - open fun mediumTextType(): String = "TEXT" - - /** Character type for storing strings of variable and _large_ length. */ - open fun largeTextType(): String = "TEXT" - - // Binary data types - - /** Binary type for storing binary strings of variable and _unlimited_ length. */ - abstract fun binaryType(): String - - /** Binary type for storing binary strings of a specific [length]. */ - open fun binaryType(length: Int): String = if (length == Int.MAX_VALUE) "VARBINARY(MAX)" else "VARBINARY($length)" - - /** Binary type for storing BLOBs. */ - open fun blobType(): String = "BLOB" - - /** Binary type for storing [UUID]. */ - open fun uuidType(): String = "BINARY(16)" - - @Suppress("MagicNumber") - open fun uuidToDB(value: UUID): Any = - ByteBuffer.allocate(16).putLong(value.mostSignificantBits).putLong(value.leastSignificantBits).array() - - // Date/Time types - - /** Data type for storing both date and time without a time zone. */ - open fun dateTimeType(): String = "DATETIME" - - /** Data type for storing both date and time with a time zone. */ - open fun timestampWithTimeZoneType(): String = "TIMESTAMP WITH TIME ZONE" - - /** Time type for storing time without a time zone. */ - open fun timeType(): String = "TIME" - - /** Data type for storing date without time or a time zone. */ - open fun dateType(): String = "DATE" - - // Boolean type - - /** Data type for storing boolean values. */ - open fun booleanType(): String = "BOOLEAN" - - /** Returns the SQL representation of the specified [bool] value. */ - open fun booleanToStatementString(bool: Boolean): String = bool.toString().uppercase() - - /** Returns the boolean value of the specified SQL [value]. */ - open fun booleanFromStringToBoolean(value: String): Boolean = value.toBoolean() - - // JSON types - - /** Data type for storing JSON in a non-binary text format. */ - open fun jsonType(): String = "JSON" - - /** Data type for storing JSON in a decomposed binary format. */ - open fun jsonBType(): String = - throw UnsupportedByDialectException("This vendor does not support binary JSON data type", currentDialect) - - // Misc. - - /** Returns the SQL representation of the specified expression, for it to be used as a column default value. */ - open fun processForDefaultValue(e: Expression<*>): String = when { - e is LiteralOp<*> && e.columnType is JsonColumnMarker -> if (currentDialect is H2Dialect) { - "$e".substringAfter("JSON ") - } else { - "'$e'" - } - e is LiteralOp<*> -> "$e" - e is Function<*> -> "$e" - currentDialect is MysqlDialect -> "$e" - currentDialect is SQLServerDialect -> "$e" - else -> "($e)" - } - - open fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) { - queryBuilder.append((expression as? ExpressionAlias<*>)?.alias ?: expression, " ", sortOrder.code) - } - - /** Returns the hex-encoded value to be inserted into the database. */ - abstract fun hexToDb(hexString: String): String -} /** * Provides definitions for all the supported SQL functions. @@ -288,7 +138,9 @@ abstract class FunctionProvider { * @param pattern Pattern the expression is checked against. * @param mode Match mode used to check the expression. */ - open fun Expression.match(pattern: String, mode: MatchMode? = null): Op = with(SqlExpressionBuilder) { + open fun Expression.match(pattern: String, mode: MatchMode? = null): Op = with( + SqlExpressionBuilder + ) { this@match.like(pattern) } @@ -831,502 +683,3 @@ abstract class FunctionProvider { } } } - -/** - * Represents metadata information about a specific column. - */ -data class ColumnMetadata( - /** Name of the column. */ - val name: String, - /** - * Type of the column. - * - * @see java.sql.Types - */ - val type: Int, - /** Whether the column if nullable or not. */ - val nullable: Boolean, - /** Optional size of the column. */ - val size: Int?, - /** Is the column auto increment */ - val autoIncrement: Boolean, - /** Default value */ - val defaultDbValue: String?, -) - -/** - * Represents metadata information about a specific table's primary key. - */ -data class PrimaryKeyMetadata( - /** Name of the primary key. */ - val name: String, - /** Names of the primary key's columns. */ - val columnNames: List -) - -/** - * Common interface for all database dialects. - */ -@Suppress("TooManyFunctions") -interface DatabaseDialect { - /** Name of this dialect. */ - val name: String - - /** Data type provider of this dialect. */ - val dataTypeProvider: DataTypeProvider - - /** Function provider of this dialect. */ - val functionProvider: FunctionProvider - - /** Returns `true` if the dialect supports the `IF EXISTS`/`IF NOT EXISTS` option when creating, altering or dropping objects, `false` otherwise. */ - val supportsIfNotExists: Boolean get() = true - - /** Returns `true` if the dialect supports the creation of sequences, `false` otherwise. */ - val supportsCreateSequence: Boolean get() = true - - /** Returns `true` if the dialect requires the use of a sequence to create an auto-increment column, `false` otherwise. */ - val needsSequenceToAutoInc: Boolean get() = false - - /** Returns the default reference option for the dialect. */ - val defaultReferenceOption: ReferenceOption get() = ReferenceOption.RESTRICT - - /** Returns `true` if the dialect requires the use of quotes when using symbols in object names, `false` otherwise. */ - val needsQuotesWhenSymbolsInNames: Boolean get() = true - - /** Returns `true` if the dialect supports returning multiple generated keys as a result of an insert operation, `false` otherwise. */ - val supportsMultipleGeneratedKeys: Boolean - - /** Returns`true` if the dialect supports returning generated keys obtained from a sequence. */ - val supportsSequenceAsGeneratedKeys: Boolean get() = supportsCreateSequence - val supportsOnlyIdentifiersInGeneratedKeys: Boolean get() = false - - /** Returns `true` if the dialect supports an upsert operation returning an affected-row value of 0, 1, or 2. */ - val supportsTernaryAffectedRowValues: Boolean get() = false - - /** Returns`true` if the dialect supports schema creation. */ - val supportsCreateSchema: Boolean get() = true - - /** Returns `true` if the dialect supports subqueries within a UNION/EXCEPT/INTERSECT statement */ - val supportsSubqueryUnions: Boolean get() = false - - val supportsDualTableConcept: Boolean get() = false - - val supportsOrderByNullsFirstLast: Boolean get() = false - - /** Returns `true` if the dialect supports window function definitions with GROUPS mode in frame clause */ - val supportsWindowFrameGroupsMode: Boolean get() = false - - val likePatternSpecialChars: Map get() = defaultLikePatternSpecialChars - - /** Returns true if autoCommit should be enabled to create/drop database */ - val requiresAutoCommitOnCreateDrop: Boolean get() = false - - /** Returns the name of the current database. */ - fun getDatabase(): String - - /** Returns a list with the names of all the defined tables. */ - fun allTablesNames(): List - - /** Checks if the specified table exists in the database. */ - fun tableExists(table: Table): Boolean - - /** Checks if the specified schema exists. */ - fun schemaExists(schema: Schema): Boolean - - fun checkTableMapping(table: Table): Boolean = true - - /** Returns a map with the column metadata of all the defined columns in each of the specified [tables]. */ - fun tableColumns(vararg tables: Table): Map> = emptyMap() - - /** Returns a map with the foreign key constraints of all the defined columns sets in each of the specified [tables]. */ - fun columnConstraints( - vararg tables: Table - ): Map>>, List> = emptyMap() - - /** Returns a map with all the defined indices in each of the specified [tables]. */ - fun existingIndices(vararg tables: Table): Map> = emptyMap() - - /** Returns a map with the primary key metadata in each of the specified [tables]. */ - fun existingPrimaryKeys(vararg tables: Table): Map = emptyMap() - - /** Returns `true` if the dialect supports `SELECT FOR UPDATE` statements, `false` otherwise. */ - fun supportsSelectForUpdate(): Boolean - - /** Returns `true` if the specified [e] is allowed as a default column value in the dialect, `false` otherwise. */ - fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = e is LiteralOp<*> - - /** Returns the catalog name of the connection of the specified [transaction]. */ - fun catalog(transaction: Transaction): String = transaction.connection.catalog - - /** Clears any cached values. */ - fun resetCaches() - - /** Clears any cached values including schema names. */ - fun resetSchemaCaches() - - // Specific SQL statements - - /** Returns the SQL command that creates the specified [index]. */ - fun createIndex(index: Index): String - - /** Returns the SQL command that drops the specified [indexName] from the specified [tableName]. */ - fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String - - /** Returns the SQL command that modifies the specified [column]. */ - fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List - - /** Returns the SQL command that adds a primary key specified [pkName] to an existing [table]. */ - fun addPrimaryKey(table: Table, pkName: String?, vararg pkColumns: Column<*>): String - - fun createDatabase(name: String) = "CREATE DATABASE IF NOT EXISTS ${name.inProperCase()}" - - fun dropDatabase(name: String) = "DROP DATABASE IF EXISTS ${name.inProperCase()}" - - fun setSchema(schema: Schema): String = "SET SCHEMA ${schema.identifier}" - - fun createSchema(schema: Schema): String = buildString { - append("CREATE SCHEMA IF NOT EXISTS ") - append(schema.identifier) - appendIfNotNull(" AUTHORIZATION ", schema.authorization) - } - - fun dropSchema(schema: Schema, cascade: Boolean): String = buildString { - append("DROP SCHEMA IF EXISTS ", schema.identifier) - - if (cascade) { - append(" CASCADE") - } - } - - companion object { - private val defaultLikePatternSpecialChars = mapOf('%' to null, '_' to null) - } -} - -sealed class ForUpdateOption(open val querySuffix: String) { - - internal object NoForUpdateOption : ForUpdateOption("") { - override val querySuffix: String get() = error("querySuffix should not be called for NoForUpdateOption object") - } - - object ForUpdate : ForUpdateOption("FOR UPDATE") - - // https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html for clarification - object MySQL { - object ForShare : ForUpdateOption("FOR SHARE") - - object LockInShareMode : ForUpdateOption("LOCK IN SHARE MODE") - } - - // https://mariadb.com/kb/en/select/#lock-in-share-modefor-update - object MariaDB { - object LockInShareMode : ForUpdateOption("LOCK IN SHARE MODE") - } - - // https://www.postgresql.org/docs/current/sql-select.html - // https://www.postgresql.org/docs/12/explicit-locking.html#LOCKING-ROWS for clarification - object PostgreSQL { - enum class MODE(val statement: String) { - NO_WAIT("NOWAIT"), SKIP_LOCKED("SKIP LOCKED") - } - - abstract class ForUpdateBase( - querySuffix: String, - private val mode: MODE? = null, - private vararg val ofTables: Table - ) : ForUpdateOption("") { - private val preparedQuerySuffix = buildString { - append(querySuffix) - ofTables.takeIf { it.isNotEmpty() }?.let { tables -> - append(" OF ") - tables.joinTo(this, separator = ",") { it.tableName } - } - mode?.let { - append(" ${it.statement}") - } - } - final override val querySuffix: String = preparedQuerySuffix - } - - class ForUpdate( - mode: MODE? = null, - vararg ofTables: Table - ) : ForUpdateBase("FOR UPDATE", mode, ofTables = ofTables) - - open class ForNoKeyUpdate( - mode: MODE? = null, - vararg ofTables: Table - ) : ForUpdateBase("FOR NO KEY UPDATE", mode, ofTables = ofTables) { - companion object : ForNoKeyUpdate() - } - - open class ForShare( - mode: MODE? = null, - vararg ofTables: Table - ) : ForUpdateBase("FOR SHARE", mode, ofTables = ofTables) { - companion object : ForShare() - } - - open class ForKeyShare( - mode: MODE? = null, - vararg ofTables: Table - ) : ForUpdateBase("FOR KEY SHARE", mode, ofTables = ofTables) { - companion object : ForKeyShare() - } - } - - // https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2066346 - object Oracle { - object ForUpdateNoWait : ForUpdateOption("FOR UPDATE NOWAIT") - - class ForUpdateWait(timeout: Int) : ForUpdateOption("FOR UPDATE WAIT $timeout") - } -} - -/** - * Base implementation of a vendor dialect - */ -abstract class VendorDialect( - override val name: String, - override val dataTypeProvider: DataTypeProvider, - override val functionProvider: FunctionProvider -) : DatabaseDialect { - - protected val identifierManager - get() = TransactionManager.current().db.identifierManager - - @Suppress("UnnecessaryAbstractClass") - abstract class DialectNameProvider(val dialectName: String) - - /* Cached values */ - private var _allTableNames: Map>? = null - private var _allSchemaNames: List? = null - - /** Returns a list with the names of all the defined tables within default scheme. */ - val allTablesNames: List - get() { - val connection = TransactionManager.current().connection - return getAllTableNamesCache().getValue(connection.metadata { currentScheme }) - } - - private fun getAllTableNamesCache(): Map> { - val connection = TransactionManager.current().connection - if (_allTableNames == null) { - _allTableNames = connection.metadata { tableNames } - } - return _allTableNames!! - } - - private fun getAllSchemaNamesCache(): List { - val connection = TransactionManager.current().connection - if (_allSchemaNames == null) { - _allSchemaNames = connection.metadata { schemaNames } - } - return _allSchemaNames!! - } - - override val supportsMultipleGeneratedKeys: Boolean = true - - override fun getDatabase(): String = catalog(TransactionManager.current()) - - /** - * Returns a list with the names of all the defined tables with schema prefixes if database supports it. - * This method always re-read data from DB. - * Using `allTablesNames` field is the preferred way. - */ - override fun allTablesNames(): List = TransactionManager.current().connection.metadata { - tableNames.getValue(currentScheme) - } - - override fun tableExists(table: Table): Boolean { - val tableScheme = table.schemaName - val scheme = tableScheme?.inProperCase() ?: TransactionManager.current().connection.metadata { currentScheme } - val allTables = getAllTableNamesCache().getValue(scheme) - return allTables.any { - when { - tableScheme != null -> it == table.nameInDatabaseCase() - scheme.isEmpty() -> it == table.nameInDatabaseCaseUnquoted() - else -> { - val sanitizedTableName = if (currentDialect is MysqlDialect) { - table.tableNameWithoutScheme - } else { - table.tableNameWithoutSchemeSanitized - } - it == "$scheme.$sanitizedTableName".inProperCase() - } - } - } - } - - override fun schemaExists(schema: Schema): Boolean { - val allSchemas = getAllSchemaNamesCache() - return allSchemas.any { it == schema.identifier.inProperCase() } - } - - override fun tableColumns(vararg tables: Table): Map> = - TransactionManager.current().connection.metadata { columns(*tables) } - - override fun columnConstraints( - vararg tables: Table - ): Map>>, List> { - val constraints = HashMap>>, MutableList>() - - val tablesToLoad = tables.filter { !columnConstraintsCache.containsKey(it.nameInDatabaseCaseUnquoted()) } - - fillConstraintCacheForTables(tablesToLoad) - tables.forEach { table -> - columnConstraintsCache[table.nameInDatabaseCaseUnquoted()].orEmpty().forEach { - constraints.getOrPut(table to it.from) { arrayListOf() }.add(it) - } - } - return constraints - } - - override fun existingIndices(vararg tables: Table): Map> = - TransactionManager.current().db.metadata { existingIndices(*tables) } - - override fun existingPrimaryKeys(vararg tables: Table): Map = - TransactionManager.current().db.metadata { existingPrimaryKeys(*tables) } - - private val supportsSelectForUpdate: Boolean by lazy { - TransactionManager.current().db.metadata { supportsSelectForUpdate } - } - - override fun supportsSelectForUpdate(): Boolean = supportsSelectForUpdate - - protected fun String.quoteIdentifierWhenWrongCaseOrNecessary(tr: Transaction): String = - tr.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(this) - - protected val columnConstraintsCache: MutableMap> = ConcurrentHashMap() - - protected open fun fillConstraintCacheForTables(tables: List
): Unit = - columnConstraintsCache.putAll(TransactionManager.current().db.metadata { tableConstraints(tables) }) - - override fun resetCaches() { - _allTableNames = null - columnConstraintsCache.clear() - TransactionManager.current().db.metadata { cleanCache() } - } - - override fun resetSchemaCaches() { - _allSchemaNames = null - resetCaches() - } - - fun filterCondition(index: Index): String? { - return index.filterCondition?.let { - when (currentDialect) { - is PostgreSQLDialect, is SQLServerDialect, is SQLiteDialect -> { - QueryBuilder(false) - .append(" WHERE ").append(it) - .toString() - } - else -> { - exposedLogger.warn("Index creation with a filter condition is not supported in ${currentDialect.name}") - return null - } - } - } ?: "" - } - - private fun indexFunctionToString(function: Function<*>): String { - val baseString = function.toString() - return when (currentDialect) { - // SQLite & Oracle do not support "." operator (with table prefix) in index expressions - is SQLiteDialect, is OracleDialect -> baseString.replace(Regex("""^*[^( ]*\."""), "") - is MysqlDialect -> if (baseString.first() != '(') "($baseString)" else baseString - else -> baseString - } - } - - /** - * Uniqueness might be required for foreign key constraints. - * - * In PostgreSQL (https://www.postgresql.org/docs/current/indexes-unique.html), UNIQUE means B-tree only. - * Unique constraints can not be partial - * Unique indexes can be partial - */ - override fun createIndex(index: Index): String { - val t = TransactionManager.current() - val quotedTableName = t.identity(index.table) - val quotedIndexName = t.db.identifierManager.cutIfNecessaryAndQuote(index.indexName) - val keyFields = index.columns.plus(index.functions ?: emptyList()) - val fieldsList = keyFields.joinToString(prefix = "(", postfix = ")") { - when (it) { - is Column<*> -> t.identity(it) - is Function<*> -> indexFunctionToString(it) - // returned by existingIndices() mapping String metadata to stringLiteral() - is LiteralOp<*> -> it.value.toString().trim('"') - else -> { - exposedLogger.warn("Unexpected defining key field will be passed as String: $it") - it.toString() - } - } - } - val includesOnlyColumns = index.functions?.isEmpty() != false - val maybeFilterCondition = filterCondition(index) ?: return "" - - return when { - // unique and no filter -> constraint, the type is not supported - index.unique && maybeFilterCondition.isEmpty() && includesOnlyColumns -> { - "ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $fieldsList" - } - // unique and filter -> index only, the type is not supported - index.unique -> { - "CREATE UNIQUE INDEX $quotedIndexName ON $quotedTableName $fieldsList$maybeFilterCondition" - } - // type -> can't be unique or constraint - index.indexType != null -> { - createIndexWithType( - name = quotedIndexName, table = quotedTableName, - columns = fieldsList, type = index.indexType, filterCondition = maybeFilterCondition - ) - } - else -> { - "CREATE INDEX $quotedIndexName ON $quotedTableName $fieldsList$maybeFilterCondition" - } - } - } - - protected open fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { - return "CREATE INDEX $name ON $table $columns USING $type$filterCondition" - } - - override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String { - return "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP CONSTRAINT ${identifierManager.quoteIfNecessary(indexName)}" - } - - override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = - listOf("ALTER TABLE ${TransactionManager.current().identity(column.table)} MODIFY COLUMN ${column.descriptionDdl(true)}") - - override fun addPrimaryKey(table: Table, pkName: String?, vararg pkColumns: Column<*>): String { - val transaction = TransactionManager.current() - val columns = pkColumns.joinToString(prefix = "(", postfix = ")") { transaction.identity(it) } - val constraint = pkName?.let { " CONSTRAINT ${identifierManager.quoteIfNecessary(it)} " } ?: " " - return "ALTER TABLE ${transaction.identity(table)} ADD${constraint}PRIMARY KEY $columns" - } -} - -private val explicitDialect = ThreadLocal() - -internal fun withDialect(dialect: DatabaseDialect, body: () -> T): T { - return try { - explicitDialect.set(dialect) - body() - } finally { - explicitDialect.set(null) - } -} - -/** Returns the dialect used in the current transaction, may throw an exception if there is no current transaction. */ -val currentDialect: DatabaseDialect get() = explicitDialect.get() ?: TransactionManager.current().db.dialect - -internal val currentDialectIfAvailable: DatabaseDialect? - get() = if (TransactionManager.isInitialized() && TransactionManager.currentOrNull() != null) { - currentDialect - } else { - null - } - -internal fun String.inProperCase(): String = - TransactionManager.currentOrNull()?.db?.identifierManager?.inProperCase(this@inProperCase) ?: this diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt index bd1f333f0a..10b3bf01ff 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt @@ -122,6 +122,9 @@ internal object H2FunctionProvider : FunctionProvider() { * H2 dialect implementation. */ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2FunctionProvider) { + + override fun toString(): String = "H2Dialect[$dialectName, $h2Mode]" + internal enum class H2MajorVersion { One, Two } @@ -259,6 +262,8 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function override fun createDatabase(name: String) = "CREATE SCHEMA IF NOT EXISTS ${name.inProperCase()}" + override fun listDatabases(): String = "SHOW SCHEMAS" + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = super.modifyColumn(column, columnDiff).map { it.replace("MODIFY COLUMN", "ALTER COLUMN") } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt similarity index 95% rename from exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt rename to exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt index 8d9cd8af62..e34f24a725 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MysqlDialect.kt @@ -59,6 +59,7 @@ internal object MysqlDataTypeProvider : DataTypeProvider() { currentDialect ) } + else -> super.processForDefaultValue(e) } @@ -381,5 +382,23 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq override fun dropSchema(schema: Schema, cascade: Boolean): String = "DROP SCHEMA IF EXISTS ${schema.identifier}" + override fun tableExists(table: Table): Boolean { + val tableScheme = table.schemaName + val scheme = tableScheme?.inProperCase() ?: TransactionManager.current().connection.metadata { currentScheme } + val allTables = getAllTableNamesCache().getValue(scheme) + + return allTables.any { + when { + tableScheme != null -> it == table.nameInDatabaseCase() + scheme.isEmpty() -> it == table.nameInDatabaseCaseUnquoted() + else -> { + val sanitizedTableName = table.tableNameWithoutScheme + val nameInDb = "$scheme.$sanitizedTableName".inProperCase() + it == nameInDb + } + } + } + } + companion object : DialectNameProvider("mysql") } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt index 90ec7b6f11..d9dac0d97a 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt @@ -333,6 +333,8 @@ open class OracleDialect : VendorDialect(dialectName, OracleDataTypeProvider, Or override fun createDatabase(name: String): String = "CREATE DATABASE ${name.inProperCase()}" + override fun listDatabases(): String = error("This operation is not supported by Oracle dialect") + override fun dropDatabase(name: String): String = "DROP DATABASE" override fun setSchema(schema: Schema): String = "ALTER SESSION SET CURRENT_SCHEMA = ${schema.identifier}" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index 63dc32ab57..5fe5df1574 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -324,6 +324,8 @@ open class PostgreSQLDialect : VendorDialect(dialectName, PostgreSQLDataTypeProv override fun createDatabase(name: String): String = "CREATE DATABASE ${name.inProperCase()}" + override fun listDatabases(): String = "SELECT datname FROM pg_database" + override fun dropDatabase(name: String): String = "DROP DATABASE ${name.inProperCase()}" override fun setSchema(schema: Schema): String = "SET search_path TO ${schema.identifier}" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata.kt new file mode 100644 index 0000000000..3b5c502ca2 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata.kt @@ -0,0 +1,11 @@ +package org.jetbrains.exposed.sql.vendors + +/** + * Represents metadata information about a specific table's primary key. + */ +data class PrimaryKeyMetadata( + /** Name of the primary key. */ + val name: String, + /** Names of the primary key's columns. */ + val columnNames: List +) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 5d12fb3d25..b399677c13 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -303,6 +303,8 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid override fun createDatabase(name: String): String = "CREATE DATABASE ${name.inProperCase()}" + override fun listDatabases(): String = "SELECT name FROM sys.databases" + override fun dropDatabase(name: String) = "DROP DATABASE ${name.inProperCase()}" override fun setSchema(schema: Schema): String = "ALTER USER ${schema.authorization} WITH DEFAULT_SCHEMA = ${schema.identifier}" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt index cc6f9926d0..481bd73d32 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt @@ -273,6 +273,8 @@ open class SQLiteDialect : VendorDialect(dialectName, SQLiteDataTypeProvider, SQ override fun createDatabase(name: String) = "ATTACH DATABASE '${name.lowercase()}.db' AS ${name.inProperCase()}" + override fun listDatabases(): String = "SELECT name FROM pragma_database_list" + override fun dropDatabase(name: String) = "DETACH DATABASE ${name.inProperCase()}" companion object : DialectNameProvider("sqlite") { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/VendorDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/VendorDialect.kt new file mode 100644 index 0000000000..b9dc21d89e --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/VendorDialect.kt @@ -0,0 +1,229 @@ +package org.jetbrains.exposed.sql.vendors + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Function +import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.util.concurrent.ConcurrentHashMap + +/** + * Base implementation of a vendor dialect + */ +abstract class VendorDialect( + override val name: String, + override val dataTypeProvider: DataTypeProvider, + override val functionProvider: FunctionProvider +) : DatabaseDialect { + + protected val identifierManager + get() = TransactionManager.current().db.identifierManager + + @Suppress("UnnecessaryAbstractClass") + abstract class DialectNameProvider(val dialectName: String) + + /* Cached values */ + private var _allTableNames: Map>? = null + private var _allSchemaNames: List? = null + + /** Returns a list with the names of all the defined tables within default scheme. */ + val allTablesNames: List + get() { + val connection = TransactionManager.current().connection + return getAllTableNamesCache().getValue(connection.metadata { currentScheme }) + } + + protected fun getAllTableNamesCache(): Map> { + val connection = TransactionManager.current().connection + if (_allTableNames == null) { + _allTableNames = connection.metadata { tableNames } + } + return _allTableNames!! + } + + private fun getAllSchemaNamesCache(): List { + val connection = TransactionManager.current().connection + if (_allSchemaNames == null) { + _allSchemaNames = connection.metadata { schemaNames } + } + return _allSchemaNames!! + } + + override val supportsMultipleGeneratedKeys: Boolean = true + + override fun getDatabase(): String = catalog(TransactionManager.current()) + + /** + * Returns a list with the names of all the defined tables with schema prefixes if database supports it. + * This method always re-read data from DB. + * Using `allTablesNames` field is the preferred way. + */ + override fun allTablesNames(): List = TransactionManager.current().connection.metadata { + tableNames.getValue(currentScheme) + } + + override fun tableExists(table: Table): Boolean { + val tableScheme = table.schemaName + val scheme = tableScheme?.inProperCase() ?: TransactionManager.current().connection.metadata { currentScheme } + val allTables = getAllTableNamesCache().getValue(scheme) + return allTables.any { + when { + tableScheme != null -> it == table.nameInDatabaseCase() + scheme.isEmpty() -> it == table.nameInDatabaseCaseUnquoted() + else -> { + val sanitizedTableName = table.tableNameWithoutSchemeSanitized + val nameInDb = "$scheme.$sanitizedTableName".inProperCase() + it == nameInDb + } + } + } + } + + override fun schemaExists(schema: Schema): Boolean { + val allSchemas = getAllSchemaNamesCache() + return allSchemas.any { it == schema.identifier.inProperCase() } + } + + override fun tableColumns(vararg tables: Table): Map> = + TransactionManager.current().connection.metadata { columns(*tables) } + + override fun columnConstraints( + vararg tables: Table + ): Map>>, List> { + val constraints = HashMap>>, MutableList>() + + val tablesToLoad = tables.filter { !columnConstraintsCache.containsKey(it.nameInDatabaseCaseUnquoted()) } + + fillConstraintCacheForTables(tablesToLoad) + tables.forEach { table -> + columnConstraintsCache[table.nameInDatabaseCaseUnquoted()].orEmpty().forEach { + constraints.getOrPut(table to it.from) { arrayListOf() }.add(it) + } + } + return constraints + } + + override fun existingIndices(vararg tables: Table): Map> = + TransactionManager.current().db.metadata { existingIndices(*tables) } + + override fun existingPrimaryKeys(vararg tables: Table): Map = + TransactionManager.current().db.metadata { existingPrimaryKeys(*tables) } + + private val supportsSelectForUpdate: Boolean by lazy { + TransactionManager.current().db.metadata { supportsSelectForUpdate } + } + + override fun supportsSelectForUpdate(): Boolean = supportsSelectForUpdate + + protected fun String.quoteIdentifierWhenWrongCaseOrNecessary(tr: Transaction): String = + tr.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(this) + + protected val columnConstraintsCache: MutableMap> = ConcurrentHashMap() + + protected open fun fillConstraintCacheForTables(tables: List
): Unit = + columnConstraintsCache.putAll(TransactionManager.current().db.metadata { tableConstraints(tables) }) + + override fun resetCaches() { + _allTableNames = null + columnConstraintsCache.clear() + TransactionManager.current().db.metadata { cleanCache() } + } + + override fun resetSchemaCaches() { + _allSchemaNames = null + resetCaches() + } + + fun filterCondition(index: Index): String? { + return index.filterCondition?.let { + when (currentDialect) { + is PostgreSQLDialect, is SQLServerDialect, is SQLiteDialect -> { + QueryBuilder(false) + .append(" WHERE ").append(it) + .toString() + } + + else -> { + exposedLogger.warn("Index creation with a filter condition is not supported in ${currentDialect.name}") + return null + } + } + } ?: "" + } + + private fun indexFunctionToString(function: Function<*>): String { + val baseString = function.toString() + return when (currentDialect) { + // SQLite & Oracle do not support "." operator (with table prefix) in index expressions + is SQLiteDialect, is OracleDialect -> baseString.replace(Regex("""^*[^( ]*\."""), "") + is MysqlDialect -> if (baseString.first() != '(') "($baseString)" else baseString + else -> baseString + } + } + + /** + * Uniqueness might be required for foreign key constraints. + * + * In PostgreSQL (https://www.postgresql.org/docs/current/indexes-unique.html), UNIQUE means B-tree only. + * Unique constraints can not be partial + * Unique indexes can be partial + */ + override fun createIndex(index: Index): String { + val t = TransactionManager.current() + val quotedTableName = t.identity(index.table) + val quotedIndexName = t.db.identifierManager.cutIfNecessaryAndQuote(index.indexName) + val keyFields = index.columns.plus(index.functions ?: emptyList()) + val fieldsList = keyFields.joinToString(prefix = "(", postfix = ")") { + when (it) { + is Column<*> -> t.identity(it) + is Function<*> -> indexFunctionToString(it) + // returned by existingIndices() mapping String metadata to stringLiteral() + is LiteralOp<*> -> it.value.toString().trim('"') + else -> { + exposedLogger.warn("Unexpected defining key field will be passed as String: $it") + it.toString() + } + } + } + val includesOnlyColumns = index.functions?.isEmpty() != false + val maybeFilterCondition = filterCondition(index) ?: return "" + + return when { + // unique and no filter -> constraint, the type is not supported + index.unique && maybeFilterCondition.isEmpty() && includesOnlyColumns -> { + "ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $fieldsList" + } + // unique and filter -> index only, the type is not supported + index.unique -> { + "CREATE UNIQUE INDEX $quotedIndexName ON $quotedTableName $fieldsList$maybeFilterCondition" + } + // type -> can't be unique or constraint + index.indexType != null -> { + createIndexWithType( + name = quotedIndexName, table = quotedTableName, + columns = fieldsList, type = index.indexType, filterCondition = maybeFilterCondition + ) + } + + else -> { + "CREATE INDEX $quotedIndexName ON $quotedTableName $fieldsList$maybeFilterCondition" + } + } + } + + protected open fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { + return "CREATE INDEX $name ON $table $columns USING $type$filterCondition" + } + + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String { + return "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP CONSTRAINT ${identifierManager.quoteIfNecessary(indexName)}" + } + + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = + listOf("ALTER TABLE ${TransactionManager.current().identity(column.table)} MODIFY COLUMN ${column.descriptionDdl(true)}") + + override fun addPrimaryKey(table: Table, pkName: String?, vararg pkColumns: Column<*>): String { + val transaction = TransactionManager.current() + val columns = pkColumns.joinToString(prefix = "(", postfix = ")") { transaction.identity(it) } + val constraint = pkName?.let { " CONSTRAINT ${identifierManager.quoteIfNecessary(it)} " } ?: " " + return "ALTER TABLE ${transaction.identity(table)} ADD${constraint}PRIMARY KEY $columns" + } +} diff --git a/exposed-java-time/build.gradle.kts b/exposed-java-time/build.gradle.kts index 5a77a74cc2..ce9cc69580 100644 --- a/exposed-java-time/build.gradle.kts +++ b/exposed-java-time/build.gradle.kts @@ -4,7 +4,6 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { kotlin("jvm") apply true kotlin("plugin.serialization") apply true - id("testWithDBs") } repositories { diff --git a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/sqlserver/SQLServerDefaultsTest.kt b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/sqlserver/SQLServerDefaultsTest.kt index 90fe9ac00d..af68845b7e 100644 --- a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/sqlserver/SQLServerDefaultsTest.kt +++ b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/sqlserver/SQLServerDefaultsTest.kt @@ -15,7 +15,6 @@ class SQLServerDefaultsTest : DatabaseTestsBase() { @Test fun testDefaultExpressionsForTemporalTable() { - fun databaseGeneratedTimestamp() = object : ExpressionWithColumnType() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { +"DEFAULT" } override val columnType: IColumnType = JavaLocalDateTimeColumnType() diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index 13b69821c9..92ffdb86ef 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -17,9 +17,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) override val databaseDialectName: String by lazyMetadata { when (driverName) { - "MySQL-AB JDBC Driver", - "MySQL Connector/J", - "MySQL Connector Java" -> MysqlDialect.dialectName + "MySQL-AB JDBC Driver", "MySQL Connector/J", "MySQL Connector Java" -> MysqlDialect.dialectName "MariaDB Connector/J" -> MariaDBDialect.dialectName "SQLite JDBC" -> SQLiteDialect.dialectName @@ -90,11 +88,9 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } override val tableNames: Map> - get() = CachableMapWithDefault( - default = { schemeName -> - tableNamesFor(schemeName) - } - ) + get() = CachableMapWithDefault(default = { schemeName -> + tableNamesFor(schemeName) + }) private fun tableNamesFor(scheme: String): List = with(metadata) { val useCatalogInsteadOfScheme = currentDialect is MysqlDialect @@ -129,18 +125,10 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) return schemas.map { identifierManager.inProperCase(it) } } - private fun ResultSet.extractColumns( - tables: Array, - extract: (ResultSet) -> Pair - ): Map> { - val mapping = tables.associateBy { it.nameInDatabaseCaseUnquoted() } - val result = HashMap>() - + private fun ResultSet.extractColumns(): List { + val result = mutableListOf() while (next()) { - val (tableName, columnMetadata) = extract(this) - mapping[tableName]?.let { t -> - result.getOrPut(t) { arrayListOf() } += columnMetadata - } + result.add(asColumnMetadata()) } return result } @@ -148,41 +136,47 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) override fun columns(vararg tables: Table): Map> { val result = mutableMapOf>() val useSchemaInsteadOfDatabase = currentDialect is MysqlDialect - val tablesBySchema = tables.groupBy { identifierManager.inProperCase(it.schemaName ?: currentScheme) } - tablesBySchema.forEach { (schema, schemaTables) -> - val catalog = if (!useSchemaInsteadOfDatabase || schema == currentScheme) databaseName else schema - val rs = metadata.getColumns(catalog, schema, "%", "%") - result += rs.extractColumns(schemaTables.toTypedArray()) { - // @see java.sql.DatabaseMetaData.getColumns - // That read should go first as Oracle driver closes connection after that - val defaultDbValue = it.getString("COLUMN_DEF")?.let { sanitizedDefault(it) } - val autoIncrement = it.getString("IS_AUTOINCREMENT") == "YES" - val type = it.getInt("DATA_TYPE") - val columnMetadata = ColumnMetadata( - it.getString("COLUMN_NAME"), - type, - it.getBoolean("NULLABLE"), - it.getInt("COLUMN_SIZE").takeIf { it != 0 }, - autoIncrement, - // Not sure this filters enough but I dont think we ever want to have sequences here - defaultDbValue?.takeIf { !autoIncrement }, - ) - it.getString("TABLE_NAME") to columnMetadata + + for ((schema, schemaTables) in tablesBySchema.entries) { + for (table in schemaTables) { + val catalog = if (!useSchemaInsteadOfDatabase || schema == currentScheme) databaseName else schema + val rs = metadata.getColumns(catalog, schema, table.nameInDatabaseCaseUnquoted(), "%") + val columns = rs.extractColumns() + check(columns.isNotEmpty()) + result[table] = columns + rs.close() } - rs.close() } + return result } + private fun ResultSet.asColumnMetadata(): ColumnMetadata { + val defaultDbValue = getString("COLUMN_DEF")?.let { sanitizedDefault(it) } + val autoIncrement = getString("IS_AUTOINCREMENT") == "YES" + val type = getInt("DATA_TYPE") + + return ColumnMetadata( + getString("COLUMN_NAME"), + type, + getBoolean("NULLABLE"), + getInt("COLUMN_SIZE").takeIf { it != 0 }, + autoIncrement, + // Not sure this filters enough but I dont think we ever want to have sequences here + defaultDbValue?.takeIf { !autoIncrement }, + ) + } + private fun sanitizedDefault(defaultValue: String): String { val dialect = currentDialect val h2Mode = dialect.h2Mode return when { dialect is SQLServerDialect -> defaultValue.trim('(', ')', '\'') dialect is OracleDialect || h2Mode == H2CompatibilityMode.Oracle -> defaultValue.trim().trim('\'') - dialect is MysqlDialect || h2Mode == H2CompatibilityMode.MySQL || h2Mode == H2CompatibilityMode.MariaDB -> - defaultValue.substringAfter("b'").trim('\'') + dialect is MysqlDialect || h2Mode == H2CompatibilityMode.MySQL || h2Mode == H2CompatibilityMode.MariaDB -> defaultValue.substringAfter( + "b'" + ).trim('\'') dialect is PostgreSQLDialect || h2Mode == H2CompatibilityMode.PostgreSQL -> when { defaultValue.startsWith('\'') && defaultValue.endsWith('\'') -> defaultValue.trim('\'') @@ -202,7 +196,11 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) val (catalog, tableSchema) = tableCatalogAndSchema(table) existingIndicesCache.getOrPut(table) { - val pkNames = metadata.getPrimaryKeys(catalog, tableSchema, table.nameInDatabaseCaseUnquoted()).let { rs -> + val pkNames = metadata.getPrimaryKeys( + catalog, + tableSchema, + table.nameInDatabaseCaseUnquoted() + ).let { rs -> val names = arrayListOf() while (rs.next()) { rs.getString("PK_NAME")?.let { names += it } @@ -223,7 +221,9 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) else -> null } columnNameMetadata?.let { columnName -> - val column = transaction.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(columnName) + val column = transaction.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary( + columnName + ) val isUnique = !rs.getBoolean("NON_UNIQUE") val isPartial = if (rs.getString("FILTER_CONDITION").isNullOrEmpty()) null else Op.TRUE tmpIndices.getOrPut(Triple(indexName, isUnique, isPartial)) { arrayListOf() }.add(column) @@ -232,20 +232,19 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } rs.close() val tColumns = table.columns.associateBy { transaction.identity(it) } - tmpIndices.filterNot { it.key.first in pkNames } - .mapNotNull { (index, columns) -> - val (functionBased, columnBased) = columns.distinct().partition { cn -> tColumns[cn] == null } - columnBased.map { cn -> tColumns[cn]!! } - .takeIf { c -> c.size + functionBased.size == columns.size } - ?.let { c -> - Index( - c, index.second, index.first, - filterCondition = index.third, - functions = functionBased.map { stringLiteral(it) }.ifEmpty { null }, - functionsTable = if (functionBased.isNotEmpty()) table else null - ) - } + tmpIndices.filterNot { it.key.first in pkNames }.mapNotNull { (index, columns) -> + val (functionBased, columnBased) = columns.distinct().partition { cn -> tColumns[cn] == null } + columnBased.map { cn -> tColumns[cn]!! }.takeIf { c -> c.size + functionBased.size == columns.size }?.let { c -> + Index( + c, + index.second, + index.first, + filterCondition = index.third, + functions = functionBased.map { stringLiteral(it) }.ifEmpty { null }, + functionsTable = if (functionBased.isNotEmpty()) table else null + ) } + } } } return HashMap(existingIndicesCache) @@ -274,7 +273,9 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) val (catalog, tableSchema) = tableCatalogAndSchema(allTables[table]!!) metadata.getImportedKeys(catalog, identifierManager.inProperCase(tableSchema), table).iterate { val fromTableName = getString("FKTABLE_NAME")!! - val fromColumnName = identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(getString("FKCOLUMN_NAME")!!) + val fromColumnName = identifierManager.quoteIdentifierWhenWrongCaseOrNecessary( + getString("FKCOLUMN_NAME")!! + ) val fromColumn = allTables[fromTableName]?.columns?.firstOrNull { identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(it.name) == fromColumnName } ?: return@iterate null // Do not crash if there are missing fields in Exposed's tables @@ -295,11 +296,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) onDelete = constraintDeleteRule, name = constraintName ) - } - .filterNotNull() - .groupBy { it.fkName } - .values - .map { it.reduce(ForeignKeyConstraint::plus) } + }.filterNotNull().groupBy { it.fkName }.values.map { it.reduce(ForeignKeyConstraint::plus) } } } diff --git a/exposed-jodatime/build.gradle.kts b/exposed-jodatime/build.gradle.kts index d736d5dab7..26a1c38928 100644 --- a/exposed-jodatime/build.gradle.kts +++ b/exposed-jodatime/build.gradle.kts @@ -1,7 +1,6 @@ plugins { kotlin("jvm") apply true kotlin("plugin.serialization") apply true - id("testWithDBs") } repositories { diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt index b046d3e13d..3853c94c19 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt @@ -47,7 +47,9 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { val clientDefault by TableWithDBDefault.clientDefault override fun equals(other: Any?): Boolean { - return (other as? DBDefault)?.let { id == it.id && field == it.field && equalDateTime(t1, it.t1) && equalDateTime(t2, it.t2) } ?: false + return (other as? DBDefault)?.let { + id == it.id && field == it.field && equalDateTime(t1, it.t1) && equalDateTime(t2, it.t2) + } ?: false } override fun hashCode(): Int = id.value.hashCode() @@ -170,6 +172,7 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { fun Expression<*>.itOrNull() = when { currentDialectTest.isAllowedAsColumnDefault(this) -> "DEFAULT ${currentDialectTest.dataTypeProvider.processForDefaultValue(this)} NOT NULL" + else -> "NULL" } @@ -191,7 +194,10 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { ")" val expected = if (currentDialectTest is OracleDialect || currentDialectTest.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { - arrayListOf("CREATE SEQUENCE t_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", baseExpression) + arrayListOf( + "CREATE SEQUENCE t_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", + baseExpression + ) } else { arrayListOf(baseExpression) } @@ -276,13 +282,13 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { } @Test - fun defaultCurrentDateTimeTest() { + fun testDefaultCurrentDateTime() { val testDate = object : IntIdTable("TestDate") { val time = datetime("time").defaultExpression(CurrentDateTime) } withTables(testDate) { - val duration: Long = 2_000 + val duration: Long = 2000 val before = currentDateTime() Thread.sleep(duration) @@ -378,6 +384,7 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { fun Expression<*>.itOrNull() = when { currentDialectTest.isAllowedAsColumnDefault(this) -> "DEFAULT ${currentDialectTest.dataTypeProvider.processForDefaultValue(this)} NOT NULL" + else -> "NULL" } diff --git a/exposed-json/build.gradle.kts b/exposed-json/build.gradle.kts index 4e9c992d4d..b5030b77be 100644 --- a/exposed-json/build.gradle.kts +++ b/exposed-json/build.gradle.kts @@ -3,7 +3,6 @@ import org.jetbrains.exposed.gradle.Versions plugins { kotlin("jvm") apply true kotlin("plugin.serialization") apply true - id("testWithDBs") } repositories { diff --git a/exposed-kotlin-datetime/build.gradle.kts b/exposed-kotlin-datetime/build.gradle.kts index 59e6f6294d..862d77b923 100644 --- a/exposed-kotlin-datetime/build.gradle.kts +++ b/exposed-kotlin-datetime/build.gradle.kts @@ -4,7 +4,6 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { kotlin("jvm") apply true kotlin("plugin.serialization") apply true - id("testWithDBs") } repositories { diff --git a/exposed-money/build.gradle.kts b/exposed-money/build.gradle.kts index abd56d702c..8a78724072 100644 --- a/exposed-money/build.gradle.kts +++ b/exposed-money/build.gradle.kts @@ -1,6 +1,5 @@ plugins { kotlin("jvm") apply true - id("testWithDBs") } repositories { diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfigurationTest.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfigurationTest.kt index d81a0b0777..849d5586c8 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfigurationTest.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfigurationTest.kt @@ -47,7 +47,10 @@ open class ExposedAutoConfigurationTest { fun `database config can be overrode by custom one`() { val expectedConfig = CustomDatabaseConfigConfiguration.expectedConfig assertSame(databaseConfig, expectedConfig) - assertEquals(expectedConfig.maxEntitiesToStoreInCachePerEntity, databaseConfig!!.maxEntitiesToStoreInCachePerEntity) + assertEquals( + expectedConfig.maxEntitiesToStoreInCachePerEntity, + databaseConfig!!.maxEntitiesToStoreInCachePerEntity + ) } @TestConfiguration @@ -120,5 +123,7 @@ open class AsyncExposedService { open fun allTestData() = TestTable.selectAll().toList() // you need to put open otherwise @Transactional is not applied since spring plugin not applied (similar to maven kotlin plugin) - open fun allTestDataAsync(): CompletableFuture> = CompletableFuture.completedFuture(TestTable.selectAll().toList()) + open fun allTestDataAsync(): CompletableFuture> = CompletableFuture.completedFuture( + TestTable.selectAll().toList() + ) } diff --git a/exposed-tests/build.gradle.kts b/exposed-tests/build.gradle.kts index e823be481f..9916f003ad 100644 --- a/exposed-tests/build.gradle.kts +++ b/exposed-tests/build.gradle.kts @@ -4,7 +4,6 @@ import org.jetbrains.exposed.gradle.Versions plugins { kotlin("jvm") apply true - id("testWithDBs") } repositories { @@ -13,22 +12,23 @@ repositories { dependencies { implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", Versions.kotlinCoroutines) + implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", Versions.kotlinCoroutines) + + implementation(kotlin("test-junit")) + implementation("junit", "junit", "4.12") + implementation("org.hamcrest", "hamcrest-library", "1.3") + implementation(project(":exposed-core")) implementation(project(":exposed-jdbc")) implementation(project(":exposed-dao")) implementation(project(":exposed-crypt")) - implementation(kotlin("test-junit")) + implementation("org.slf4j", "slf4j-api", Versions.slf4j) implementation("org.apache.logging.log4j", "log4j-slf4j-impl", Versions.log4j2) implementation("org.apache.logging.log4j", "log4j-api", Versions.log4j2) implementation("org.apache.logging.log4j", "log4j-core", Versions.log4j2) - implementation("junit", "junit", "4.12") - implementation("org.hamcrest", "hamcrest-library", "1.3") - implementation("com.zaxxer", "HikariCP", "5.0.1") - implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", Versions.kotlinCoroutines) - implementation("org.testcontainers", "mysql", Versions.testContainers) - implementation("org.testcontainers", "postgresql", Versions.testContainers) + implementation("com.zaxxer", "HikariCP", "5.0.1") testCompileOnly("org.postgresql", "postgresql", Versions.postgre) testCompileOnly("com.impossibl.pgjdbc-ng", "pgjdbc-ng", Versions.postgreNG) compileOnly("com.h2database", "h2", Versions.h2) @@ -37,8 +37,9 @@ dependencies { } tasks.withType().configureEach { - if (JavaVersion.VERSION_1_8 > JavaVersion.current()) + if (JavaVersion.VERSION_1_8 > JavaVersion.current()) { jvmArgs = listOf("-XX:MaxPermSize=256m") + } testLogging { events.addAll(listOf(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED)) showStandardStreams = true diff --git a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt index 38ed727a20..fa00ec0ecb 100644 --- a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt @@ -1,7 +1,10 @@ package org.jetbrains.exposed.sql.tests -import org.h2.engine.Mode -import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Key +import org.jetbrains.exposed.sql.Schema +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.statements.StatementInterceptor import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction import org.jetbrains.exposed.sql.transactions.nullableTransactionScope @@ -10,168 +13,23 @@ import org.jetbrains.exposed.sql.transactions.transactionManager import org.jetbrains.exposed.sql.vendors.H2Dialect import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.junit.Assume -import org.junit.AssumptionViolatedException -import org.testcontainers.containers.MySQLContainer -import org.testcontainers.containers.PostgreSQLContainer +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters import java.math.BigDecimal -import java.sql.Connection -import java.sql.SQLException -import java.time.Duration import java.util.* import kotlin.concurrent.thread -import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.full.declaredMemberProperties -enum class TestDB( - val connection: () -> String, - val driver: String, - val user: String = "root", - val pass: String = "", - val beforeConnection: () -> Unit = {}, - val afterTestFinished: () -> Unit = {}, - val dbConfig: DatabaseConfig.Builder.() -> Unit = {} -) { - - H2({ "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;" }, "org.h2.Driver", dbConfig = { - defaultIsolationLevel = Connection.TRANSACTION_READ_COMMITTED - }), - H2_MYSQL( - { "jdbc:h2:mem:mysql;MODE=MySQL;DB_CLOSE_DELAY=-1" }, "org.h2.Driver", - beforeConnection = { - Mode::class.declaredMemberProperties.firstOrNull { it.name == "convertInsertNullToZero" }?.let { field -> - val mode = Mode.getInstance("MySQL") - (field as KMutableProperty1).set(mode, false) - } - } - ), - H2_MARIADB({ "jdbc:h2:mem:mariadb;MODE=MariaDB;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1" }, "org.h2.Driver"), - H2_PSQL({ "jdbc:h2:mem:psql;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1" }, "org.h2.Driver"), - H2_ORACLE({ "jdbc:h2:mem:oracle;MODE=Oracle;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1" }, "org.h2.Driver"), - H2_SQLSERVER({ "jdbc:h2:mem:sqlserver;MODE=MSSQLServer;DB_CLOSE_DELAY=-1" }, "org.h2.Driver"), - SQLITE({ "jdbc:sqlite:file:test?mode=memory&cache=shared" }, "org.sqlite.JDBC"), - MYSQL( - connection = { - if (runTestContainersMySQL()) { - "${mySQLProcess.jdbcUrl}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useSSL=false&zeroDateTimeBehavior=convertToNull" - } else { - val host = System.getProperty("exposed.test.mysql.host") ?: System.getProperty("exposed.test.mysql8.host") - val port = System.getProperty("exposed.test.mysql.port") ?: System.getProperty("exposed.test.mysql8.port") - host.let { dockerHost -> - "jdbc:mysql://$dockerHost:$port/testdb?useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" - } - } - }, - user = "root", - pass = if (runTestContainersMySQL()) "test" else "", - driver = "com.mysql.jdbc.Driver", - beforeConnection = { if (runTestContainersMySQL()) mySQLProcess }, - afterTestFinished = { if (runTestContainersMySQL()) mySQLProcess.close() } - ), - POSTGRESQL( - { "${postgresSQLProcess.jdbcUrl}&user=postgres&password=&lc_messages=en_US.UTF-8" }, "org.postgresql.Driver", - beforeConnection = { postgresSQLProcess }, afterTestFinished = { postgresSQLProcess.close() } - ), - POSTGRESQLNG( - { "${postgresSQLProcess.jdbcUrl.replaceFirst(":postgresql:", ":pgsql:")}&user=postgres&password=" }, "com.impossibl.postgres.jdbc.PGDriver", - user = "postgres", beforeConnection = { postgresSQLProcess }, afterTestFinished = { postgresSQLProcess.close() } - ), - ORACLE( - driver = "oracle.jdbc.OracleDriver", user = "ExposedTest", pass = "12345", - connection = { - "jdbc:oracle:thin:@//${System.getProperty("exposed.test.oracle.host", "localhost")}" + - ":${System.getProperty("exposed.test.oracle.port", "1521")}/XEPDB1" - }, - beforeConnection = { - Locale.setDefault(Locale.ENGLISH) - val tmp = Database.connect(ORACLE.connection(), user = "sys as sysdba", password = "Oracle18", driver = ORACLE.driver) - transaction(Connection.TRANSACTION_READ_COMMITTED, db = tmp) { - repetitionAttempts = 1 - try { - exec("DROP USER ExposedTest CASCADE") - } catch (e: Exception) { // ignore - exposedLogger.warn("Exception on deleting ExposedTest user") - } - exec("CREATE USER ExposedTest ACCOUNT UNLOCK IDENTIFIED BY 12345") - exec("grant all privileges to ExposedTest") - } - Unit - } - ), - - SQLSERVER( - { - "jdbc:sqlserver://${System.getProperty("exposed.test.sqlserver.host", "192.168.99.100")}" + - ":${System.getProperty("exposed.test.sqlserver.port", "32781")}" - }, - "com.microsoft.sqlserver.jdbc.SQLServerDriver", "SA", "yourStrong(!)Password" - ), - - MARIADB( - { - "jdbc:mariadb://${System.getProperty("exposed.test.mariadb.host", "192.168.99.100")}" + - ":${System.getProperty("exposed.test.mariadb.port", "3306")}/testdb" - }, - "org.mariadb.jdbc.Driver" - ); - - var db: Database? = null - - fun connect(configure: DatabaseConfig.Builder.() -> Unit = {}): Database { - val config = DatabaseConfig { - dbConfig() - configure() - } - return Database.connect(connection(), databaseConfig = config, user = user, password = pass, driver = driver) - } - - companion object { - val allH2TestDB = listOf(H2, H2_MYSQL, H2_PSQL, H2_MARIADB, H2_ORACLE, H2_SQLSERVER) - val mySqlRelatedDB = listOf(MYSQL, MARIADB, H2_MYSQL, H2_MARIADB) - fun enabledInTests(): Set { - val concreteDialects = System.getProperty("exposed.test.dialects", "") - .split(",") - .mapTo(HashSet()) { it.trim().uppercase() } - return values().filterTo(enumSetOf()) { it.name in concreteDialects } - } - } -} +val TEST_DIALECTS: HashSet = System.getProperty( + "exposed.test.dialects", + "" +).split(",").mapTo(HashSet()) { it.trim().uppercase() } private val registeredOnShutdown = HashSet() -private val postgresSQLProcess by lazy { - PostgreSQLContainer("postgres:13.8-alpine") - .withUsername("postgres") - .withPassword("") - .withDatabaseName("template1") - .withStartupTimeout(Duration.ofSeconds(60)) - .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust") - .apply { - listOf( - "timezone=UTC", - "synchronous_commit=off", - "max_connections=300", - "fsync=off" - ).forEach{ - setCommand("postgres", "-c", it) - } - start() - } -} - -private val mySQLProcess by lazy { - MySQLContainer("mysql:5") - .withDatabaseName("testdb") - .withEnv("MYSQL_ROOT_PASSWORD", "test") - .withExposedPorts().apply { - start() - } -} - -private fun runTestContainersMySQL(): Boolean = - (System.getProperty("exposed.test.mysql.host") ?: System.getProperty("exposed.test.mysql8.host")).isNullOrBlank() - internal var currentTestDB by nullableTransactionScope() +@RunWith(Parameterized::class) abstract class DatabaseTestsBase { init { TimeZone.setDefault(TimeZone.getTimeZone("UTC")) @@ -183,13 +41,27 @@ abstract class DatabaseTestsBase { } } - fun withDb(dbSettings: TestDB, statement: Transaction.(TestDB) -> Unit) { - try { - Assume.assumeTrue(dbSettings in TestDB.enabledInTests()) - } catch (e: AssumptionViolatedException) { - exposedLogger.warn("$dbSettings is not enabled for being used in tests", e) - throw e + companion object { + @Parameters(name = "name: {2}, container: {0}, dialect: {1}") + @JvmStatic + fun data(): Collection> { + val name = System.getProperty("exposed.test.name") + val container = System.getProperty("exposed.test.container") + return TestDB.enabledDialects().map { arrayOf(container, it, name) } } + } + + @Parameterized.Parameter(0) + lateinit var container: String + + @Parameterized.Parameter(1) + lateinit var dialect: TestDB + + @Parameterized.Parameter(2) + lateinit var testName: String + + fun withDb(dbSettings: TestDB, statement: Transaction.(TestDB) -> Unit) { + Assume.assumeTrue(dialect == dbSettings) if (dbSettings !in registeredOnShutdown) { dbSettings.beforeConnection() @@ -204,53 +76,55 @@ abstract class DatabaseTestsBase { } val database = dbSettings.db!! - try { - transaction(database.transactionManager.defaultIsolationLevel, db = database) { - repetitionAttempts = 1 - registerInterceptor(CurrentTestDBInterceptor) - currentTestDB = dbSettings - statement(dbSettings) - } - } catch (e: SQLException) { - throw e - } catch (e: Exception) { - throw Exception("Failed on ${dbSettings.name}", e) + transaction(database.transactionManager.defaultIsolationLevel, db = database) { + repetitionAttempts = 1 + registerInterceptor(CurrentTestDBInterceptor) + currentTestDB = dbSettings + statement(dbSettings) } } fun withDb(db: List? = null, excludeSettings: List = emptyList(), statement: Transaction.(TestDB) -> Unit) { - val enabledInTests = TestDB.enabledInTests() - val toTest = db?.intersect(enabledInTests) ?: (enabledInTests - excludeSettings) - Assume.assumeTrue(toTest.isNotEmpty()) - toTest.forEach { dbSettings -> - @Suppress("TooGenericExceptionCaught") - try { - withDb(dbSettings, statement) - } catch (e: Exception) { - throw AssertionError("Failed on ${dbSettings.name}", e) - } + if (db != null && dialect !in db) { + Assume.assumeFalse(true) + return + } + + if (dialect in excludeSettings) { + Assume.assumeFalse(true) + return } + + if (dialect !in TestDB.enabledDialects()) { + Assume.assumeFalse(true) + return + } + + withDb(dialect, statement) } fun withTables(excludeSettings: List, vararg tables: Table, statement: Transaction.(TestDB) -> Unit) { - val toTest = TestDB.enabledInTests() - excludeSettings - Assume.assumeTrue(toTest.isNotEmpty()) - toTest.forEach { testDB -> - withDb(testDB) { - SchemaUtils.create(*tables) + Assume.assumeFalse(dialect in excludeSettings) + + withDb(dialect) { + try { + SchemaUtils.drop(*tables) + } catch (_: Throwable) { + } + + SchemaUtils.create(*tables) + try { + statement(dialect) + commit() // Need commit to persist data before drop tables + } finally { try { - statement(testDB) - commit() // Need commit to persist data before drop tables - } finally { - try { + SchemaUtils.drop(*tables) + commit() + } catch (_: Exception) { + val database = dialect.db!! + inTopLevelTransaction(database.transactionManager.defaultIsolationLevel, db = database) { + repetitionAttempts = 1 SchemaUtils.drop(*tables) - commit() - } catch (_: Exception) { - val database = testDB.db!! - inTopLevelTransaction(database.transactionManager.defaultIsolationLevel, db = database) { - repetitionAttempts = 1 - SchemaUtils.drop(*tables) - } } } } @@ -258,30 +132,38 @@ abstract class DatabaseTestsBase { } fun withSchemas(excludeSettings: List, vararg schemas: Schema, statement: Transaction.() -> Unit) { - val toTest = TestDB.enabledInTests() - excludeSettings - Assume.assumeTrue(toTest.isNotEmpty()) - toTest.forEach { testDB -> - withDb(testDB) { - if (currentDialectTest.supportsCreateSchema) { - SchemaUtils.createSchema(*schemas) - try { - statement() - commit() // Need commit to persist data before drop schemas - } finally { - val cascade = it != TestDB.SQLSERVER - SchemaUtils.dropSchema(*schemas, cascade = cascade) - commit() - } + if (dialect !in TestDB.enabledDialects()) { + Assume.assumeFalse(true) + return + } + + if (dialect in excludeSettings) { + Assume.assumeFalse(true) + return + } + + withDb(dialect) { + if (currentDialectTest.supportsCreateSchema) { + SchemaUtils.createSchema(*schemas) + try { + statement() + commit() // Need commit to persist data before drop schemas + } finally { + val cascade = it != TestDB.SQLSERVER + SchemaUtils.dropSchema(*schemas, cascade = cascade) + commit() } } } } - fun withTables(vararg tables: Table, statement: Transaction.(TestDB) -> Unit) = + fun withTables(vararg tables: Table, statement: Transaction.(TestDB) -> Unit) { withTables(excludeSettings = emptyList(), tables = tables, statement = statement) + } - fun withSchemas(vararg schemas: Schema, statement: Transaction.() -> Unit) = + fun withSchemas(vararg schemas: Schema, statement: Transaction.() -> Unit) { withSchemas(excludeSettings = emptyList(), schemas = schemas, statement = statement) + } fun addIfNotExistsIfSupported() = if (currentDialectTest.supportsIfNotExists) { "IF NOT EXISTS " @@ -297,7 +179,11 @@ abstract class DatabaseTestsBase { fun Transaction.isOldMySql(version: String = "8.0") = currentDialectTest is MysqlDialect && !db.isVersionCovers(BigDecimal(version)) - protected fun prepareSchemaForTest(schemaName: String) : Schema { - return Schema(schemaName, defaultTablespace = "USERS", temporaryTablespace = "TEMP ", quota = "20M", on = "USERS") - } + protected fun prepareSchemaForTest(schemaName: String): Schema = Schema( + schemaName, + defaultTablespace = "USERS", + temporaryTablespace = "TEMP ", + quota = "20M", + on = "USERS" + ) } diff --git a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/LogDbInTestName.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/LogDbInTestName.kt new file mode 100644 index 0000000000..15906d6627 --- /dev/null +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/LogDbInTestName.kt @@ -0,0 +1,16 @@ +package org.jetbrains.exposed.sql.tests + +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +abstract class LogDbInTestName { + @Parameterized.Parameter(0) + lateinit var dialect: String + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = TestDB.enabledDialects().map { arrayOf(it.name) } + } +} diff --git a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/TestDB.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/TestDB.kt new file mode 100644 index 0000000000..1dcf014c15 --- /dev/null +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/TestDB.kt @@ -0,0 +1,121 @@ +package org.jetbrains.exposed.sql.tests + +import org.h2.engine.Mode +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.DatabaseConfig +import org.jetbrains.exposed.sql.exposedLogger +import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Connection +import java.util.* +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.declaredMemberProperties + +enum class TestDB( + val connection: () -> String, + val driver: String, + val user: String = "root", + val pass: String = "Exposed_password_1!", + val beforeConnection: () -> Unit = {}, + val afterTestFinished: () -> Unit = {}, + val dbConfig: DatabaseConfig.Builder.() -> Unit = {} +) { + H2({ "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;" }, "org.h2.Driver", dbConfig = { + defaultIsolationLevel = Connection.TRANSACTION_READ_COMMITTED + }), + H2_MYSQL({ "jdbc:h2:mem:mysql;MODE=MySQL;DB_CLOSE_DELAY=-1" }, "org.h2.Driver", beforeConnection = { + Mode::class.declaredMemberProperties.firstOrNull { it.name == "convertInsertNullToZero" }?.let { field -> + val mode = Mode.getInstance("MySQL") + @Suppress("UNCHECKED_CAST") + (field as KMutableProperty1).set(mode, false) + } + }), + H2_MARIADB( + { "jdbc:h2:mem:mariadb;MODE=MariaDB;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1" }, + "org.h2.Driver", + pass = "root" + ), + H2_PSQL( + { "jdbc:h2:mem:psql;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1" }, + "org.h2.Driver" + ), + H2_ORACLE( + { "jdbc:h2:mem:oracle;MODE=Oracle;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1" }, + "org.h2.Driver" + ), + H2_SQLSERVER({ "jdbc:h2:mem:sqlserver;MODE=MSSQLServer;DB_CLOSE_DELAY=-1" }, "org.h2.Driver"), + SQLITE({ "jdbc:sqlite:file:test?mode=memory&cache=shared" }, "org.sqlite.JDBC"), + MYSQL( + connection = { + "jdbc:mysql://127.0.0.1:3001/testdb?useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" + }, + driver = "com.mysql.jdbc.Driver" + ), + POSTGRESQL( + { "jdbc:postgresql://127.0.0.1:3004/postgres?lc_messages=en_US.UTF-8" }, + "org.postgresql.Driver", + beforeConnection = { }, + afterTestFinished = { } + ), + POSTGRESQLNG( + { POSTGRESQL.connection().replace(":postgresql:", ":pgsql:") }, + "com.impossibl.postgres.jdbc.PGDriver", + ), + ORACLE(driver = "oracle.jdbc.OracleDriver", user = "ExposedTest", pass = "12345", connection = { + "jdbc:oracle:thin:@127.0.0.1:3003/XEPDB1" + }, beforeConnection = { + Locale.setDefault(Locale.ENGLISH) + val tmp = Database.connect( + ORACLE.connection(), + user = "sys as sysdba", + password = "Oracle18", + driver = ORACLE.driver + ) + transaction(Connection.TRANSACTION_READ_COMMITTED, db = tmp) { + repetitionAttempts = 1 + try { + exec("DROP USER ExposedTest CASCADE") + } catch (e: Exception) { // ignore + exposedLogger.warn("Exception on deleting ExposedTest user") + } + exec("CREATE USER ExposedTest ACCOUNT UNLOCK IDENTIFIED BY 12345") + exec("grant all privileges to ExposedTest") + } + Unit + }), + SQLSERVER( + { + "jdbc:sqlserver://127.0.0.1:3005" + }, + "com.microsoft.sqlserver.jdbc.SQLServerDriver", + "SA", + ), + MARIADB( + { + "jdbc:mariadb://127.0.0.1:3000/testdb" + }, + "org.mariadb.jdbc.Driver" + ); + + var db: Database? = null + + fun connect(configure: DatabaseConfig.Builder.() -> Unit = {}): Database { + val config = DatabaseConfig { + dbConfig() + configure() + } + return Database.connect(connection(), databaseConfig = config, user = user, password = pass, driver = driver) + } + + companion object { + val allH2TestDB = listOf(H2, H2_MYSQL, H2_PSQL, H2_MARIADB, H2_ORACLE, H2_SQLSERVER) + val mySqlRelatedDB = listOf(MYSQL, MARIADB, H2_MYSQL, H2_MARIADB) + + fun enabledDialects(): Set { + if (TEST_DIALECTS.isEmpty()) { + return values().toSet() + } + + return values().filterTo(enumSetOf()) { it.name in TEST_DIALECTS } + } + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/dao/SamplesDao.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/dao/SamplesDao.kt index b8d2ef4f46..2ccf735fa6 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/dao/SamplesDao.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/dao/SamplesDao.kt @@ -35,7 +35,7 @@ class City(id: EntityID) : IntEntity(id) { } fun main() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "") transaction { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/sql/SamplesSQL.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/sql/SamplesSQL.kt index 76a5eff111..b6bd555b12 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/sql/SamplesSQL.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/demo/sql/SamplesSQL.kt @@ -24,7 +24,7 @@ object Cities : Table() { } fun main() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "") transaction { @@ -114,20 +114,21 @@ fun main() { println("Functions and group by:") - ((Cities innerJoin Users) - .slice(Cities.name, Users.id.count()) - .selectAll() - .groupBy(Cities.name) + ( + (Cities innerJoin Users) + .slice(Cities.name, Users.id.count()) + .selectAll() + .groupBy(Cities.name) ).forEach { - val cityName = it[Cities.name] - val userCount = it[Users.id.count()] + val cityName = it[Cities.name] + val userCount = it[Users.id.count()] - if (userCount > 0) { - println("$userCount user(s) live(s) in $cityName") - } else { - println("Nobody lives in $cityName") - } + if (userCount > 0) { + println("$userCount user(s) live(s) in $cityName") + } else { + println("Nobody lives in $cityName") } + } SchemaUtils.drop(Users, Cities) } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/ConnectionPoolTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/ConnectionPoolTests.kt index 5f68e15bc8..c74af6b1dc 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/ConnectionPoolTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/ConnectionPoolTests.kt @@ -11,6 +11,7 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.tests.LogDbInTestName import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction @@ -18,7 +19,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Assume import org.junit.Test -class ConnectionPoolTests { +class ConnectionPoolTests : LogDbInTestName() { private val hikariDataSource1 by lazy { HikariDataSource( HikariConfig().apply { @@ -34,7 +35,7 @@ class ConnectionPoolTests { @Test fun testSuspendTransactionsExceedingPoolSize() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) transaction(db = hikariDB1) { SchemaUtils.create(TestTable) } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/EntityReferenceCacheTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/EntityReferenceCacheTest.kt index 5f8beee6d3..e5169f6e98 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/EntityReferenceCacheTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/EntityReferenceCacheTest.kt @@ -47,7 +47,7 @@ class EntityReferenceCacheTest : DatabaseTestsBase() { } private fun executeOnH2(vararg tables: Table, body: () -> Unit) { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) var testWasStarted = false transaction(db) { SchemaUtils.create(*tables) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/H2Tests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/H2Tests.kt index c54810679c..7df91e9fe2 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/H2Tests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/H2Tests.kt @@ -18,7 +18,6 @@ class H2Tests : DatabaseTestsBase() { @Test fun insertInH2() { withDb(listOf(TestDB.H2_MYSQL, TestDB.H2)) { - SchemaUtils.drop(Testing) SchemaUtils.create(Testing) Testing.insert { @@ -33,7 +32,6 @@ class H2Tests : DatabaseTestsBase() { @Test fun replaceAsInsertInH2() { withDb(listOf(TestDB.H2_MYSQL, TestDB.H2_MARIADB)) { - SchemaUtils.drop(Testing) SchemaUtils.create(Testing) Testing.replace { @@ -76,8 +74,14 @@ class H2Tests : DatabaseTestsBase() { withDb(listOf(TestDB.H2, TestDB.H2_MYSQL)) { try { SchemaUtils.createMissingTablesAndColumns(initialTable) - assertEquals("ALTER TABLE ${tableName.inProperCase()} ADD ${"id".inProperCase()} ${t.id.columnType.sqlType()}", t.id.ddl.first()) - assertEquals("ALTER TABLE ${tableName.inProperCase()} ADD CONSTRAINT pk_$tableName PRIMARY KEY (${"id".inProperCase()})", t.id.ddl[1]) + assertEquals( + "ALTER TABLE ${tableName.inProperCase()} ADD ${"id".inProperCase()} ${t.id.columnType.sqlType()}", + t.id.ddl.first() + ) + assertEquals( + "ALTER TABLE ${tableName.inProperCase()} ADD CONSTRAINT pk_$tableName PRIMARY KEY (${"id".inProperCase()})", + t.id.ddl[1] + ) assertEquals(1, currentDialectTest.tableColumns(t)[t]!!.size) SchemaUtils.createMissingTablesAndColumns(t) assertEquals(2, currentDialectTest.tableColumns(t)[t]!!.size) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseEntityTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseEntityTest.kt index 94c864b9b3..1b20ab9d30 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseEntityTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseEntityTest.kt @@ -37,7 +37,7 @@ class MultiDatabaseEntityTest { @Before fun before() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) if (TransactionManager.isInitialized()) { currentDB = TransactionManager.currentOrNull()?.db } @@ -51,7 +51,7 @@ class MultiDatabaseEntityTest { @After fun after() { - if (TestDB.H2 in TestDB.enabledInTests()) { + if (TestDB.H2 in TestDB.enabledDialects()) { TransactionManager.resetCurrent(currentDB?.transactionManager) transaction(db1) { SchemaUtils.drop(EntityTestsData.XTable, EntityTestsData.YTable) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseTest.kt index 915873fb43..27c7f29df0 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/MultiDatabaseTest.kt @@ -39,7 +39,7 @@ class MultiDatabaseTest { @Before fun before() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) if (TransactionManager.isInitialized()) { currentDB = TransactionManager.currentOrNull()?.db } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionExceptions.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionExceptions.kt new file mode 100644 index 0000000000..eb15ea246e --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionExceptions.kt @@ -0,0 +1,189 @@ +package org.jetbrains.exposed.sql.tests.shared + +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.After +import org.junit.Assume +import org.junit.Test +import java.sql.Connection +import java.sql.DriverManager +import java.sql.SQLException +import java.sql.SQLTransientException +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.test.fail + +class ConnectionExceptions { + + abstract class ConnectionSpy(private val connection: Connection) : Connection by connection { + var commitCalled = false + var rollbackCalled = false + var closeCalled = false + + override fun commit() { + commitCalled = true + throw CommitException() + } + + override fun rollback() { + rollbackCalled = true + } + + override fun close() { + closeCalled = true + } + } + + private class WrappingDataSource(private val testDB: TestDB, private val connectionDecorator: (Connection) -> T) : DataSourceStub() { + val connections = mutableListOf() + + override fun getConnection(): Connection { + val connection = DriverManager.getConnection(testDB.connection(), testDB.user, testDB.pass) + val wrapped = connectionDecorator(connection) + connections.add(wrapped) + return wrapped + } + } + + private class RollbackException : SQLTransientException() + private class ExceptionOnRollbackConnection(connection: Connection) : ConnectionSpy(connection) { + override fun rollback() { + super.rollback() + throw RollbackException() + } + } + + @Test + fun `transaction repetition works even if rollback throws exception`() { + `_transaction repetition works even if rollback throws exception`(::ExceptionOnRollbackConnection) + } + + private fun `_transaction repetition works even if rollback throws exception`(connectionDecorator: (Connection) -> ConnectionSpy) { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + Class.forName(TestDB.H2.driver).newInstance() + + val wrappingDataSource = WrappingDataSource(TestDB.H2, connectionDecorator) + val db = Database.connect(datasource = wrappingDataSource) + try { + transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { + repetitionAttempts = 5 + this.exec("BROKEN_SQL_THAT_CAUSES_EXCEPTION()") + } + fail("Should have thrown an exception") + } catch (e: SQLException) { + MatcherAssert.assertThat(e.toString(), Matchers.containsString("BROKEN_SQL_THAT_CAUSES_EXCEPTION")) + assertEquals(5, wrappingDataSource.connections.size) + wrappingDataSource.connections.forEach { + assertFalse(it.commitCalled) + assertTrue(it.rollbackCalled) + assertTrue(it.closeCalled) + } + } + } + + private class CommitException : SQLTransientException() + private class ExceptionOnCommitConnection(connection: Connection) : ConnectionSpy(connection) { + override fun commit() { + super.commit() + throw CommitException() + } + } + + @Test + fun `transaction repetition works when commit throws exception`() { + `_transaction repetition works when commit throws exception`(::ExceptionOnCommitConnection) + } + + private fun `_transaction repetition works when commit throws exception`(connectionDecorator: (Connection) -> ConnectionSpy) { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + Class.forName(TestDB.H2.driver).newInstance() + + val wrappingDataSource = WrappingDataSource(TestDB.H2, connectionDecorator) + val db = Database.connect(datasource = wrappingDataSource) + try { + transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { + repetitionAttempts = 5 + this.exec("SELECT 1;") + } + fail("Should have thrown an exception") + } catch (_: CommitException) { + assertEquals(5, wrappingDataSource.connections.size) + wrappingDataSource.connections.forEach { + assertTrue(it.commitCalled) + assertTrue(it.closeCalled) + } + } + } + + @Test + fun `transaction throws exception if all commits throws exception`() { + `_transaction throws exception if all commits throws exception`(::ExceptionOnCommitConnection) + } + + private fun `_transaction throws exception if all commits throws exception`(connectionDecorator: (Connection) -> ConnectionSpy) { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + Class.forName(TestDB.H2.driver).newInstance() + + val wrappingDataSource = WrappingDataSource(TestDB.H2, connectionDecorator) + val db = Database.connect(datasource = wrappingDataSource) + try { + transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { + repetitionAttempts = 5 + this.exec("SELECT 1;") + } + fail("Should have thrown an exception") + } catch (_: CommitException) { + // Yay + } + } + + private class CloseException : SQLTransientException() + private class ExceptionOnRollbackCloseConnection(connection: Connection) : ConnectionSpy(connection) { + override fun rollback() { + super.rollback() + throw RollbackException() + } + + override fun close() { + super.close() + throw CloseException() + } + } + + @Test + fun `transaction repetition works even if rollback and close throws exception`() { + `_transaction repetition works even if rollback throws exception`(::ExceptionOnRollbackCloseConnection) + } + + @Test + fun `transaction repetition works when commit and close throws exception`() { + `_transaction repetition works when commit throws exception`(::ExceptionOnCommitConnection) + } + + private class ExceptionOnCommitCloseConnection(connection: Connection) : ConnectionSpy(connection) { + override fun commit() { + super.commit() + throw CommitException() + } + + override fun close() { + super.close() + throw CloseException() + } + } + + @Test + fun `transaction throws exception if all commits and close throws exception`() { + `_transaction throws exception if all commits throws exception`(::ExceptionOnCommitCloseConnection) + } + + @After + fun `teardown`() { + TransactionManager.resetCurrent(null) + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionTimeoutTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionTimeoutTest.kt new file mode 100644 index 0000000000..b949993032 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionTimeoutTest.kt @@ -0,0 +1,79 @@ +package org.jetbrains.exposed.sql.tests.shared + +import org.jetbrains.exposed.exceptions.ExposedSQLException +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.DatabaseConfig +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.Test +import java.sql.Connection +import java.sql.SQLTransientException +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +class ConnectionTimeoutTest : DatabaseTestsBase() { + + private class ExceptionOnGetConnectionDataSource : DataSourceStub() { + var connectCount = 0 + + override fun getConnection(): Connection { + connectCount++ + throw GetConnectException() + } + } + + private class GetConnectException : SQLTransientException() + + @Test + fun `connect fail causes repeated connect attempts`() { + val datasource = ExceptionOnGetConnectionDataSource() + val db = Database.connect(datasource = datasource) + + try { + transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { + repetitionAttempts = 42 + exec("SELECT 1;") + // NO OP + } + fail("Should have thrown ${GetConnectException::class.simpleName}") + } catch (e: ExposedSQLException) { + assertTrue(e.cause is GetConnectException) + assertEquals(42, datasource.connectCount) + } + } + + @Test + fun testTransactionRepetitionWithDefaults() { + val datasource = ExceptionOnGetConnectionDataSource() + val db = Database.connect( + datasource = datasource, + databaseConfig = DatabaseConfig { + defaultRepetitionAttempts = 10 + } + ) + + try { + // transaction block should use default DatabaseConfig values when no property is set + transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { + exec("SELECT 1;") + } + fail("Should have thrown ${GetConnectException::class.simpleName}") + } catch (cause: ExposedSQLException) { + assertEquals(10, datasource.connectCount) + } + + datasource.connectCount = 0 // reset connection count + + try { + // property set in transaction block should override default DatabaseConfig + transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { + repetitionAttempts = 25 + exec("SELECT 1;") + } + fail("Should have thrown ${GetConnectException::class.simpleName}") + } catch (cause: ExposedSQLException) { + assertEquals(25, datasource.connectCount) + } + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt index 5aa1ed30f2..925847e4db 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt @@ -495,7 +495,8 @@ class DDLTests : DatabaseTestsBase() { val testTable = object : Table("TestTable") { val number = integer("number") - val blobWithDefault = blob("blobWithDefault").default(defaultBlob) + val blobWithDefault = blob("blobWithDefault") + .default(defaultBlob) } withDb { testDb -> @@ -506,6 +507,7 @@ class DDLTests : DatabaseTestsBase() { } } else -> { + SchemaUtils.drop(testTable) SchemaUtils.create(testTable) testTable.insert { @@ -959,10 +961,14 @@ class DDLTests : DatabaseTestsBase() { val one = prepareSchemaForTest("one") val two = prepareSchemaForTest("two") withSchemas(two, one) { + SchemaUtils.drop(TableFromSchemeOne) SchemaUtils.create(TableFromSchemeOne) + if (currentDialectTest is OracleDialect) { exec("GRANT REFERENCES ON ${TableFromSchemeOne.tableName} to TWO") } + + SchemaUtils.drop(TableFromSchemeTwo) SchemaUtils.create(TableFromSchemeTwo) val idFromOne = TableFromSchemeOne.insertAndGetId { } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DataSourceStub.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DataSourceStub.kt new file mode 100644 index 0000000000..dc565eec3a --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DataSourceStub.kt @@ -0,0 +1,41 @@ +package org.jetbrains.exposed.sql.tests.shared + +import java.io.PrintWriter +import java.sql.Connection +import java.util.logging.Logger +import javax.sql.DataSource + +internal open class DataSourceStub : DataSource { + override fun setLogWriter(out: PrintWriter?): Unit = throw NotImplementedError() + override fun getParentLogger(): Logger { + throw NotImplementedError() + } + + override fun setLoginTimeout(seconds: Int) { + throw NotImplementedError() + } + + override fun isWrapperFor(iface: Class<*>?): Boolean { + throw NotImplementedError() + } + + override fun getLogWriter(): PrintWriter { + throw NotImplementedError() + } + + override fun unwrap(iface: Class?): T { + throw NotImplementedError() + } + + override fun getConnection(): Connection { + throw NotImplementedError() + } + + override fun getConnection(username: String?, password: String?): Connection { + throw NotImplementedError() + } + + override fun getLoginTimeout(): Int { + throw NotImplementedError() + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/RollbackTransactionTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/RollbackTransactionTest.kt new file mode 100644 index 0000000000..444577ecd2 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/RollbackTransactionTest.kt @@ -0,0 +1,62 @@ +package org.jetbrains.exposed.sql.tests.shared + +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.transactions.transactionManager +import org.junit.Test + +class RollbackTransactionTest : DatabaseTestsBase() { + + @Test + fun testRollbackWithoutSavepoints() { + withTables(RollbackTable) { + inTopLevelTransaction(db.transactionManager.defaultIsolationLevel) { + repetitionAttempts = 1 + RollbackTable.insert { it[RollbackTable.value] = "before-dummy" } + transaction { + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) + RollbackTable.insert { it[RollbackTable.value] = "inner-dummy" } + } + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) + RollbackTable.insert { it[RollbackTable.value] = "after-dummy" } + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) + rollback() + } + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) + } + } + + @Test + fun testRollbackWithSavepoints() { + withTables(RollbackTable) { + try { + db.useNestedTransactions = true + inTopLevelTransaction(db.transactionManager.defaultIsolationLevel) { + repetitionAttempts = 1 + RollbackTable.insert { it[RollbackTable.value] = "before-dummy" } + transaction { + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) + RollbackTable.insert { it[RollbackTable.value] = "inner-dummy" } + rollback() + } + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) + RollbackTable.insert { it[RollbackTable.value] = "after-dummy" } + assertEquals(1L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) + rollback() + } + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) + assertEquals(0L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) + } finally { + db.useNestedTransactions = false + } + } + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/SchemaTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/SchemaTests.kt index ef84442132..ff7e68fe2b 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/SchemaTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/SchemaTests.kt @@ -78,8 +78,10 @@ class SchemaTests : DatabaseTestsBase() { val firstCatalogName = connection.catalog + exec("DROP TABLE IF EXISTS test") exec("CREATE TABLE test(id INT PRIMARY KEY)") SchemaUtils.setSchema(schema) + exec("DROP TABLE IF EXISTS test") exec("CREATE TABLE test(id INT REFERENCES $firstCatalogName.test(id))") val catalogName = connection.catalog @@ -132,7 +134,7 @@ class SchemaTests : DatabaseTestsBase() { @Test fun `test default schema`() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) val schema = Schema("schema") TestDB.H2.connect() diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ThreadLocalManagerTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ThreadLocalManagerTest.kt index 7c8b2b828a..3ebffe49e8 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ThreadLocalManagerTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ThreadLocalManagerTest.kt @@ -1,10 +1,10 @@ package org.jetbrains.exposed.sql.tests.shared -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers import org.jetbrains.exposed.dao.id.IntIdTable -import org.jetbrains.exposed.exceptions.ExposedSQLException -import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.shared.dml.DMLTestsData @@ -12,265 +12,18 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transactionManager -import org.junit.After import org.junit.Assume import org.junit.Test -import java.io.PrintWriter -import java.sql.Connection -import java.sql.DriverManager -import java.sql.SQLException -import java.sql.SQLTransientException -import java.util.logging.Logger -import javax.sql.DataSource import kotlin.concurrent.thread -import kotlin.test.* - -private open class DataSourceStub : DataSource { - override fun setLogWriter(out: PrintWriter?): Unit = throw NotImplementedError() - override fun getParentLogger(): Logger { throw NotImplementedError() } - override fun setLoginTimeout(seconds: Int) { throw NotImplementedError() } - override fun isWrapperFor(iface: Class<*>?): Boolean { throw NotImplementedError() } - override fun getLogWriter(): PrintWriter { throw NotImplementedError() } - override fun unwrap(iface: Class?): T { throw NotImplementedError() } - override fun getConnection(): Connection { throw NotImplementedError() } - override fun getConnection(username: String?, password: String?): Connection { throw NotImplementedError() } - override fun getLoginTimeout(): Int { throw NotImplementedError() } -} - -class ConnectionTimeoutTest : DatabaseTestsBase() { - - private class ExceptionOnGetConnectionDataSource : DataSourceStub() { - var connectCount = 0 - - override fun getConnection(): Connection { - connectCount++ - throw GetConnectException() - } - } - - private class GetConnectException : SQLTransientException() - - @Test - fun `connect fail causes repeated connect attempts`() { - val datasource = ExceptionOnGetConnectionDataSource() - val db = Database.connect(datasource = datasource) - - try { - transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { - repetitionAttempts = 42 - exec("SELECT 1;") - // NO OP - } - fail("Should have thrown ${GetConnectException::class.simpleName}") - } catch (e: ExposedSQLException) { - assertTrue(e.cause is GetConnectException) - assertEquals(42, datasource.connectCount) - } - } - - @Test - fun testTransactionRepetitionWithDefaults() { - val datasource = ExceptionOnGetConnectionDataSource() - val db = Database.connect(datasource = datasource, databaseConfig = DatabaseConfig { - defaultRepetitionAttempts = 10 - }) - - try { - // transaction block should use default DatabaseConfig values when no property is set - transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { - exec("SELECT 1;") - } - fail("Should have thrown ${GetConnectException::class.simpleName}") - } catch (cause: ExposedSQLException) { - assertEquals(10, datasource.connectCount) - } - - datasource.connectCount = 0 // reset connection count - - try { - // property set in transaction block should override default DatabaseConfig - transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { - repetitionAttempts = 25 - exec("SELECT 1;") - } - fail("Should have thrown ${GetConnectException::class.simpleName}") - } catch (cause: ExposedSQLException) { - assertEquals(25, datasource.connectCount) - } - } -} - -class ConnectionExceptions { - - abstract class ConnectionSpy(private val connection: Connection) : Connection by connection { - var commitCalled = false - var rollbackCalled = false - var closeCalled = false - - override fun commit() { - commitCalled = true - throw CommitException() - } - - override fun rollback() { - rollbackCalled = true - } - - override fun close() { - closeCalled = true - } - } - - private class WrappingDataSource(private val testDB: TestDB, private val connectionDecorator: (Connection) -> T) : DataSourceStub() { - val connections = mutableListOf() - - override fun getConnection(): Connection { - val connection = DriverManager.getConnection(testDB.connection(), testDB.user, testDB.pass) - val wrapped = connectionDecorator(connection) - connections.add(wrapped) - return wrapped - } - } - - private class RollbackException : SQLTransientException() - private class ExceptionOnRollbackConnection(connection: Connection) : ConnectionSpy(connection) { - override fun rollback() { - super.rollback() - throw RollbackException() - } - } - - @Test - fun `transaction repetition works even if rollback throws exception`() { - `_transaction repetition works even if rollback throws exception`(::ExceptionOnRollbackConnection) - } - private fun `_transaction repetition works even if rollback throws exception`(connectionDecorator: (Connection) -> ConnectionSpy) { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) - Class.forName(TestDB.H2.driver).newInstance() - - val wrappingDataSource = WrappingDataSource(TestDB.H2, connectionDecorator) - val db = Database.connect(datasource = wrappingDataSource) - try { - transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { - repetitionAttempts = 5 - this.exec("BROKEN_SQL_THAT_CAUSES_EXCEPTION()") - } - fail("Should have thrown an exception") - } catch (e: SQLException) { - assertThat(e.toString(), Matchers.containsString("BROKEN_SQL_THAT_CAUSES_EXCEPTION")) - assertEquals(5, wrappingDataSource.connections.size) - wrappingDataSource.connections.forEach { - assertFalse(it.commitCalled) - assertTrue(it.rollbackCalled) - assertTrue(it.closeCalled) - } - } - } - - private class CommitException : SQLTransientException() - private class ExceptionOnCommitConnection(connection: Connection) : ConnectionSpy(connection) { - override fun commit() { - super.commit() - throw CommitException() - } - } - - @Test - fun `transaction repetition works when commit throws exception`() { - `_transaction repetition works when commit throws exception`(::ExceptionOnCommitConnection) - } - private fun `_transaction repetition works when commit throws exception`(connectionDecorator: (Connection) -> ConnectionSpy) { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) - Class.forName(TestDB.H2.driver).newInstance() - - val wrappingDataSource = WrappingDataSource(TestDB.H2, connectionDecorator) - val db = Database.connect(datasource = wrappingDataSource) - try { - transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { - repetitionAttempts = 5 - this.exec("SELECT 1;") - } - fail("Should have thrown an exception") - } catch (_: CommitException) { - assertEquals(5, wrappingDataSource.connections.size) - wrappingDataSource.connections.forEach { - assertTrue(it.commitCalled) - assertTrue(it.closeCalled) - } - } - } - - @Test - fun `transaction throws exception if all commits throws exception`() { - `_transaction throws exception if all commits throws exception`(::ExceptionOnCommitConnection) - } - private fun `_transaction throws exception if all commits throws exception`(connectionDecorator: (Connection) -> ConnectionSpy) { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) - Class.forName(TestDB.H2.driver).newInstance() - - val wrappingDataSource = WrappingDataSource(TestDB.H2, connectionDecorator) - val db = Database.connect(datasource = wrappingDataSource) - try { - transaction(Connection.TRANSACTION_SERIALIZABLE, db = db) { - repetitionAttempts = 5 - this.exec("SELECT 1;") - } - fail("Should have thrown an exception") - } catch (_: CommitException) { - // Yay - } - } - - private class CloseException : SQLTransientException() - private class ExceptionOnRollbackCloseConnection(connection: Connection) : ConnectionSpy(connection) { - override fun rollback() { - super.rollback() - throw RollbackException() - } - - override fun close() { - super.close() - throw CloseException() - } - } - - @Test - fun `transaction repetition works even if rollback and close throws exception`() { - `_transaction repetition works even if rollback throws exception`(::ExceptionOnRollbackCloseConnection) - } - - @Test - fun `transaction repetition works when commit and close throws exception`() { - `_transaction repetition works when commit throws exception`(::ExceptionOnCommitConnection) - } - - private class ExceptionOnCommitCloseConnection(connection: Connection) : ConnectionSpy(connection) { - override fun commit() { - super.commit() - throw CommitException() - } - - override fun close() { - super.close() - throw CloseException() - } - } - - @Test - fun `transaction throws exception if all commits and close throws exception`() { - `_transaction throws exception if all commits throws exception`(::ExceptionOnCommitCloseConnection) - } - - @After - fun `teardown`() { - TransactionManager.resetCurrent(null) - } -} +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertNotEquals +import kotlin.test.fail class ThreadLocalManagerTest : DatabaseTestsBase() { @Test fun testReconnection() { - Assume.assumeTrue(TestDB.MYSQL in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.MYSQL in TestDB.enabledDialects()) var secondThreadTm: TransactionManager? = null val db1 = TestDB.MYSQL.connect() @@ -315,90 +68,3 @@ class ThreadLocalManagerTest : DatabaseTestsBase() { object RollbackTable : IntIdTable() { val value = varchar("value", 20) } - -class RollbackTransactionTest : DatabaseTestsBase() { - - @Test - fun testRollbackWithoutSavepoints() { - withTables(RollbackTable) { - inTopLevelTransaction(db.transactionManager.defaultIsolationLevel) { - repetitionAttempts = 1 - RollbackTable.insert { it[value] = "before-dummy" } - transaction { - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) - RollbackTable.insert { it[value] = "inner-dummy" } - } - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) - RollbackTable.insert { it[value] = "after-dummy" } - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) - rollback() - } - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) - } - } - - @Test - fun testRollbackWithSavepoints() { - withTables(RollbackTable) { - try { - db.useNestedTransactions = true - inTopLevelTransaction(db.transactionManager.defaultIsolationLevel) { - repetitionAttempts = 1 - RollbackTable.insert { it[value] = "before-dummy" } - transaction { - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) - RollbackTable.insert { it[value] = "inner-dummy" } - rollback() - } - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) - RollbackTable.insert { it[value] = "after-dummy" } - assertEquals(1L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) - rollback() - } - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "before-dummy" }.count()) - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "inner-dummy" }.count()) - assertEquals(0L, RollbackTable.select { RollbackTable.value eq "after-dummy" }.count()) - } finally { - db.useNestedTransactions = false - } - } - } -} - -class TransactionIsolationTest : DatabaseTestsBase() { - @Test - fun `test what transaction isolation was applied`() { - withDb { - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { - repetitionAttempts = 1 - assertEquals(Connection.TRANSACTION_SERIALIZABLE, this.connection.transactionIsolation) - } - } - } -} - -class TransactionManagerResetTest { - @Test - fun `test closeAndUnregister with next Database-connect works fine`() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) - val initialManager = TransactionManager.manager - val db1 = TestDB.H2.connect() - val db1TransactionManager = TransactionManager.managerFor(db1) - assertEquals(initialManager, TransactionManager.manager) - transaction(db1) { - assertEquals(db1TransactionManager, TransactionManager.manager) - exec("SELECT 1 from dual;") - } - TransactionManager.closeAndUnregister(db1) - assertEquals(initialManager, TransactionManager.manager) - val db2 = TestDB.H2.connect() - // Check should be made in a separate thread as in current thread manager is already initialized - thread { - assertEquals(TransactionManager.managerFor(db2), TransactionManager.manager) - }.join() - } -} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/TransactionIsolationTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/TransactionIsolationTest.kt new file mode 100644 index 0000000000..d57d172db9 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/TransactionIsolationTest.kt @@ -0,0 +1,18 @@ +package org.jetbrains.exposed.sql.tests.shared + +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction +import org.junit.Test +import java.sql.Connection + +class TransactionIsolationTest : DatabaseTestsBase() { + @Test + fun `test what transaction isolation was applied`() { + withDb { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 + assertEquals(Connection.TRANSACTION_SERIALIZABLE, this.connection.transactionIsolation) + } + } + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/TransactionManagerResetTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/TransactionManagerResetTest.kt new file mode 100644 index 0000000000..16ed4835df --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/TransactionManagerResetTest.kt @@ -0,0 +1,68 @@ +package org.jetbrains.exposed.sql.tests.shared + +import junit.framework.TestCase.assertSame +import org.jetbrains.exposed.sql.tests.LogDbInTestName +import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.Assume +import org.junit.Test +import java.util.concurrent.atomic.AtomicReference +import kotlin.concurrent.thread +import kotlin.test.Ignore +import kotlin.test.assertEquals + +class TransactionManagerResetTest : LogDbInTestName() { + /** + * When the test is running alone it will have NonInitializedTransactionManager as a manager. + * After the first connection is established, the manager will be initialized and get back after the connection is closed. + * + * When the test is running in a suite, the manager will be initialized before the first connection is established. + * After the first connect it will, not change because the first manager will be used by default. + * + * This tests depends on the order of tests in the suite, so it will be disabled until we find a better solution. + */ + @Test + @Ignore + fun `test closeAndUnregister with next Database-connect works fine`() { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + + val fail = AtomicReference(null) + thread { + try { + val initialManager = TransactionManager.manager + val db1 = TestDB.H2.connect() + val db1TransactionManager = TransactionManager.managerFor(db1) + + val afterDb1Connect = TransactionManager.manager + assertSame(db1TransactionManager, afterDb1Connect) + + transaction(db1) { + assertEquals(db1TransactionManager, TransactionManager.manager) + exec("SELECT 1 from dual;") + } + + TransactionManager.closeAndUnregister(db1) + assertSame(initialManager, TransactionManager.manager) + val db2 = TestDB.H2.connect() + + // Check should be made in a separate thread as in current thread manager is already initialized + thread { + try { + assertEquals(TransactionManager.managerFor(db2), TransactionManager.manager) + } catch (cause: Throwable) { + fail.set(cause) + throw cause + } finally { + TransactionManager.closeAndUnregister(db2) + } + }.join() + } catch (cause: Throwable) { + fail.set(cause) + throw cause + } + }.join() + + fail.get()?.let { throw it } + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateDatabaseTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateDatabaseTest.kt index 1846e38a42..3e87f1bc8a 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateDatabaseTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateDatabaseTest.kt @@ -3,8 +3,10 @@ package org.jetbrains.exposed.sql.tests.shared.ddl import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.junit.Test import java.sql.SQLException +import kotlin.test.assertFailsWith class CreateDatabaseTest : DatabaseTestsBase() { @@ -23,7 +25,54 @@ class CreateDatabaseTest : DatabaseTestsBase() { } @Test - fun testCreateAndDropDatabaseInPostgresql() { + fun testListDatabasesOracle() { + withDb(TestDB.ORACLE) { + assertFailsWith { + SchemaUtils.listDatabases() + } + } + } + + @Test + fun testListDatabasesWithAutoCommit() { + withDb(listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.SQLSERVER)) { + connection.autoCommit = true + val dbName = "jetbrains" + val initial = SchemaUtils.listDatabases() + if (dbName in initial) { + SchemaUtils.dropDatabase(dbName) + } + + SchemaUtils.createDatabase(dbName) + val created = SchemaUtils.listDatabases() + assertTrue(dbName in created) + SchemaUtils.dropDatabase(dbName) + val deleted = SchemaUtils.listDatabases() + assertTrue(dbName !in deleted) + connection.autoCommit = false + } + } + + @Test + fun testListDatabases() { + withDb(excludeSettings = listOf(TestDB.ORACLE, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.SQLSERVER)) { + val dbName = "jetbrains" + val initial = SchemaUtils.listDatabases() + if (dbName in initial) { + SchemaUtils.dropDatabase(dbName) + } + + SchemaUtils.createDatabase(dbName) + val created = SchemaUtils.listDatabases() + assertTrue(dbName in created) + SchemaUtils.dropDatabase(dbName) + val deleted = SchemaUtils.listDatabases() + assertTrue(dbName !in deleted) + } + } + + @Test + fun testCreateAndDropDatabaseWithAutoCommit() { // PostgreSQL needs auto commit to be "ON" to allow create database statement withDb(listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG)) { connection.autoCommit = true diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt index a302e426ff..0cdeb39ba2 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt @@ -45,7 +45,10 @@ class CreateIndexTests : DatabaseTestsBase() { val byNameHash = index("test_table_by_name", isUnique = false, name, indexType = "HASH") } - withTables(excludeSettings = listOf(TestDB.H2_MYSQL, TestDB.SQLSERVER, TestDB.ORACLE), tables = arrayOf(TestTable)) { + withTables( + excludeSettings = listOf(TestDB.H2_MYSQL, TestDB.SQLSERVER, TestDB.ORACLE), + tables = arrayOf(TestTable) + ) { SchemaUtils.createMissingTablesAndColumns(TestTable) assertTrue(TestTable.exists()) } @@ -126,7 +129,10 @@ class CreateIndexTests : DatabaseTestsBase() { val filter = it.getString("FILTER_CONDITION") when (it.getString("INDEX_NAME")) { - "partialindextabletest_value_name" -> assertEquals(filter, "(((name)::text = 'aaa'::text) AND (value >= 6))") + "partialindextabletest_value_name" -> assertEquals( + filter, + "(((name)::text = 'aaa'::text) AND (value >= 6))" + ) "flag_index" -> assertEquals(filter, "(flag = true)") "partialindextabletest_anothervalue_unique" -> assertTrue(filter.startsWith(" UNIQUE INDEX ")) } @@ -134,10 +140,22 @@ class CreateIndexTests : DatabaseTestsBase() { kotlin.test.assertEquals(totalIndexCount, 3, "Indexes expected to be created") } - val dropIndex = Index(columns = listOf(partialIndexTable.value, partialIndexTable.name), unique = false).dropStatement().first() - kotlin.test.assertTrue(dropIndex.startsWith("DROP INDEX "), "Unique partial index must be created and dropped as index") - val dropUniqueConstraint = Index(columns = listOf(partialIndexTable.anotherValue), unique = true).dropStatement().first() - kotlin.test.assertTrue(dropUniqueConstraint.startsWith("ALTER TABLE "), "Unique index must be created and dropped as constraint") + val dropIndex = Index( + columns = listOf(partialIndexTable.value, partialIndexTable.name), + unique = false + ).dropStatement().first() + kotlin.test.assertTrue( + dropIndex.startsWith("DROP INDEX "), + "Unique partial index must be created and dropped as index" + ) + val dropUniqueConstraint = Index( + columns = listOf(partialIndexTable.anotherValue), + unique = true + ).dropStatement().first() + kotlin.test.assertTrue( + dropUniqueConstraint.startsWith("ALTER TABLE "), + "Unique index must be created and dropped as constraint" + ) execInBatch(listOf(dropUniqueConstraint, dropIndex)) @@ -177,7 +195,13 @@ class CreateIndexTests : DatabaseTestsBase() { var indices = getIndices(tester) assertEquals(3, indices.size) - val uniqueWithPartial = Index(listOf(tester.team), true, "team_only_index", null, Op.TRUE).dropStatement().first() + val uniqueWithPartial = Index( + listOf(tester.team), + true, + "team_only_index", + null, + Op.TRUE + ).dropStatement().first() val dropStatements = indices.map { it.dropStatement().first() } expect(Unit) { execInBatch(dropStatements + uniqueWithPartial) } @@ -191,7 +215,11 @@ class CreateIndexTests : DatabaseTestsBase() { else -> null } val typedPartialIndex = Index( - listOf(tester.name), false, "name_only_index", type, tester.name neq "Default" + listOf(tester.name), + false, + "name_only_index", + type, + tester.name neq "Default" ) val createdIndex = SchemaUtils.createIndex(typedPartialIndex).single() assertTrue(createdIndex.startsWith("CREATE ")) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt index 313ac950b8..ece3a8abd0 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt @@ -539,7 +539,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { uniqueIndex("index2", value2, value1) } } - + @Test fun testCreateTableWithReferenceMultipleTimes() { withTables(PlayerTable, SessionsTable) { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DMLTestData.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DMLTestData.kt index 0aad742d12..330729e21f 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DMLTestData.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DMLTestData.kt @@ -51,7 +51,11 @@ object DMLTestsData { @Suppress("LongMethod") fun DatabaseTestsBase.withCitiesAndUsers( exclude: List = emptyList(), - statement: Transaction.(cities: DMLTestsData.Cities, users: DMLTestsData.Users, userData: DMLTestsData.UserData) -> Unit + statement: Transaction.( + cities: DMLTestsData.Cities, + users: DMLTestsData.Users, + userData: DMLTestsData.UserData + ) -> Unit ) { val Users = DMLTestsData.Users val UserFlags = DMLTestsData.Users.Flags diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DeleteTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DeleteTests.kt index 68c45ab5f5..a5fbbf9ab4 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DeleteTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/DeleteTests.kt @@ -16,7 +16,13 @@ import org.junit.Test class DeleteTests : DatabaseTestsBase() { private val notSupportLimit by lazy { - val exclude = arrayListOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.ORACLE, TestDB.H2_PSQL, TestDB.H2_ORACLE) + val exclude = arrayListOf( + TestDB.POSTGRESQL, + TestDB.POSTGRESQLNG, + TestDB.ORACLE, + TestDB.H2_PSQL, + TestDB.H2_ORACLE + ) if (!SQLiteDialect.ENABLE_UPDATE_DELETE_LIMIT) { exclude.add(TestDB.SQLITE) } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt index a43c1dca35..f9631bbea2 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt @@ -466,7 +466,7 @@ class InsertTests : DatabaseTestsBase() { val testTable = object : IntIdTable("TestRollback") { val foo = integer("foo").check { it greater 0 } } - val dbToTest = TestDB.enabledInTests() - setOfNotNull( + val dbToTest = TestDB.enabledDialects() - setOfNotNull( TestDB.SQLITE, TestDB.MYSQL.takeIf { System.getProperty("exposed.test.mysql8.port") == null } ) @@ -499,7 +499,7 @@ class InsertTests : DatabaseTestsBase() { val testTable = object : IntIdTable("TestRollback") { val foo = integer("foo").check { it greater 0 } } - val dbToTest = TestDB.enabledInTests() - setOfNotNull( + val dbToTest = TestDB.enabledDialects() - setOfNotNull( TestDB.SQLITE, TestDB.MYSQL.takeIf { System.getProperty("exposed.test.mysql8.port") == null } ) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/OrderByTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/OrderByTests.kt index 1d3a6e1384..b283eca766 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/OrderByTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/OrderByTests.kt @@ -2,7 +2,6 @@ package org.jetbrains.exposed.sql.tests.shared.dml import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.tests.DatabaseTestsBase -import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.vendors.H2Dialect @@ -39,8 +38,11 @@ class OrderByTests : DatabaseTestsBase() { assertEquals(5, r.size) val usersWithoutCities = listOf("alex", "smth") val otherUsers = listOf("eugene", "sergey", "andrey") - val expected = if (isNullFirst()) usersWithoutCities + otherUsers - else otherUsers + usersWithoutCities + val expected = if (isNullFirst()) { + usersWithoutCities + otherUsers + } else { + otherUsers + usersWithoutCities + } expected.forEachIndexed { index, e -> assertEquals(e, r[index][users.id]) } @@ -54,8 +56,11 @@ class OrderByTests : DatabaseTestsBase() { assertEquals(5, r.size) val usersWithoutCities = listOf("alex", "smth") val otherUsers = listOf("eugene", "sergey", "andrey") - val expected = if (isNullFirst()) usersWithoutCities + otherUsers - else otherUsers + usersWithoutCities + val expected = if (isNullFirst()) { + usersWithoutCities + otherUsers + } else { + otherUsers + usersWithoutCities + } expected.forEachIndexed { index, e -> assertEquals(e, r[index][users.id]) } @@ -65,7 +70,10 @@ class OrderByTests : DatabaseTestsBase() { @Test fun testOrderBy04() { withCitiesAndUsers { cities, users, _ -> - val r = (cities innerJoin users).slice(cities.name, users.id.count()).selectAll().groupBy(cities.name).orderBy(cities.name).toList() + val r = (cities innerJoin users).slice( + cities.name, + users.id.count() + ).selectAll().groupBy(cities.name).orderBy(cities.name).toList() assertEquals(2, r.size) assertEquals("Munich", r[0][cities.name]) assertEquals(2, r[0][users.id.count()]) @@ -81,8 +89,11 @@ class OrderByTests : DatabaseTestsBase() { assertEquals(5, r.size) val usersWithoutCities = listOf("alex", "smth") val otherUsers = listOf("eugene", "sergey", "andrey") - val expected = if (isNullFirst()) usersWithoutCities + otherUsers - else otherUsers + usersWithoutCities + val expected = if (isNullFirst()) { + usersWithoutCities + otherUsers + } else { + otherUsers + usersWithoutCities + } expected.forEachIndexed { index, e -> assertEquals(e, r[index][users.id]) } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityCacheTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityCacheTests.kt index 2d25d92379..669912a43f 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityCacheTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityCacheTests.kt @@ -30,7 +30,7 @@ class EntityCacheTests : DatabaseTestsBase() { @Test fun testGlobalEntityCacheLimit() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) val entitiesCount = 25 val cacheSize = 10 val db = TestDB.H2.connect { @@ -60,7 +60,7 @@ class EntityCacheTests : DatabaseTestsBase() { @Test fun testGlobalEntityCacheLimitZero() { - Assume.assumeTrue(TestDB.H2 in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) val entitiesCount = 25 val db = TestDB.H2.connect() val dbNoCache = TestDB.H2.connect { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt index 6851b4ea72..81dab11b89 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt @@ -83,12 +83,14 @@ object EntityTestsData { var x by YTable.x val b: BEntity? by BEntity.backReferencedOn(XTable.y1) var content by YTable.blob + companion object : EntityClass(YTable) } } class EntityTests : DatabaseTestsBase() { - @Test fun testDefaults01() { + @Test + fun testDefaults01() { withTables(EntityTestsData.YTable, EntityTestsData.XTable) { val x = EntityTestsData.XEntity.new { } assertEquals(x.b1, true, "b1 mismatched") @@ -96,7 +98,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testDefaults02() { + @Test + fun testDefaults02() { withTables(EntityTestsData.YTable, EntityTestsData.XTable) { val a: EntityTestsData.AEntity = EntityTestsData.AEntity.create(false, EntityTestsData.XType.A) val b: EntityTestsData.BEntity = EntityTestsData.AEntity.create(false, EntityTestsData.XType.B) as EntityTestsData.BEntity @@ -135,7 +138,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testTextFieldOutsideTheTransaction() { + @Test + fun testTextFieldOutsideTheTransaction() { val objectsToVerify = arrayListOf>() withTables(Humans) { testDb -> val y1 = Human.new { @@ -152,7 +156,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testNewWithIdAndRefresh() { + @Test + fun testNewWithIdAndRefresh() { val objectsToVerify = arrayListOf>() withTables(listOf(TestDB.SQLSERVER), Humans) { testDb -> val x = Human.new(2) { @@ -362,7 +367,7 @@ class EntityTests : DatabaseTestsBase() { val board2 = Board.new { name = "irrelevant2" } assertNotNull(Board.testCache(board2.id)) Boards.update({ Boards.id eq board2.id }) { - it[Boards.name] = "relevant2" + it[name] = "relevant2" } assertNull(Board.testCache(board2.id)) board2.refresh(flush = false) @@ -378,6 +383,7 @@ class EntityTests : DatabaseTestsBase() { class Item(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(Items) + var name by Items.name var price by Items.price } @@ -434,6 +440,7 @@ class EntityTests : DatabaseTestsBase() { open class Human(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(Humans) + var h by Humans.h } @@ -505,7 +512,8 @@ class EntityTests : DatabaseTestsBase() { } // https://github.com/JetBrains/Exposed/issues/439 - @Test fun callLimitOnRelationDoesntMutateTheCachedValue() { + @Test + fun callLimitOnRelationDoesntMutateTheCachedValue() { withTables(Posts) { val category1 = Category.new { title = "cat1" @@ -530,7 +538,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testOrderByOnEntities() { + @Test + fun testOrderByOnEntities() { withTables(Categories) { Categories.deleteAll() val category1 = Category.new { title = "Test1" } @@ -543,7 +552,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun `test what update of inserted entities goes before an insert`() { + @Test + fun `test what update of inserted entities goes before an insert`() { withTables(Categories, Posts) { val category1 = Category.new { title = "category1" @@ -581,6 +591,7 @@ class EntityTests : DatabaseTestsBase() { class Parent(id: EntityID) : LongEntity(id) { companion object : LongEntityClass(Parents) + var name by Parents.name } @@ -591,11 +602,13 @@ class EntityTests : DatabaseTestsBase() { class Child(id: EntityID) : LongEntity(id) { companion object : LongEntityClass(Children) + var parent by Parent referencedOn Children.companyId var name by Children.name } - @Test fun `test new(id) with get`() { + @Test + fun `test new(id) with get`() { // SQL Server doesn't support an explicit id for auto-increment table withTables(listOf(TestDB.SQLSERVER), Parents, Children) { val parentId = Parent.new { @@ -614,7 +627,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun `newly created entity flushed successfully`() { + @Test + fun `newly created entity flushed successfully`() { withTables(Boards) { val board = Board.new { name = "Board1" }.apply { assertEquals(true, flush()) @@ -627,7 +641,8 @@ class EntityTests : DatabaseTestsBase() { private fun newTransaction(statement: Transaction.() -> T) = inTopLevelTransaction(TransactionManager.manager.defaultIsolationLevel, false, null, null, statement) - @Test fun sharingEntityBetweenTransactions() { + @Test + fun sharingEntityBetweenTransactions() { withTables(Humans) { val human1 = newTransaction { repetitionAttempts = 1 @@ -712,8 +727,10 @@ class EntityTests : DatabaseTestsBase() { override fun hashCode(): Int = id.hashCode() } + class Student(id: EntityID) : ComparableLongEntity(id) { companion object : LongEntityClass(Students) + var name by Students.name var school by School referencedOn Students.school val notes by Note.referrersOn(Notes.student, true) @@ -723,18 +740,21 @@ class EntityTests : DatabaseTestsBase() { class StudentBio(id: EntityID) : ComparableLongEntity(id) { companion object : LongEntityClass(StudentBios) + var student by Student.referencedOn(StudentBios.student) var dateOfBirth by StudentBios.dateOfBirth } class Note(id: EntityID) : ComparableLongEntity(id) { companion object : LongEntityClass(Notes) + var text by Notes.text var student by Student referencedOn Notes.student } class Detention(id: EntityID) : ComparableLongEntity(id) { companion object : LongEntityClass(Detentions) + var reason by Detentions.reason var student by Student optionalReferencedOn Detentions.student } @@ -1300,7 +1320,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun `test reference cache doesn't fully invalidated on set entity reference`() { + @Test + fun `test reference cache doesn't fully invalidated on set entity reference`() { withTables(Regions, Schools, Students, StudentBios) { val region1 = Region.new { name = "United States" @@ -1331,7 +1352,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun `test nested entity initialization`() { + @Test + fun `test nested entity initialization`() { withTables(Posts, Categories, Boards) { val post = Post.new { parent = Post.new { @@ -1356,13 +1378,15 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun `test explicit entity constructor`() { + @Test + fun `test explicit entity constructor`() { var createBoardCalled = false fun createBoard(id: EntityID): Board { createBoardCalled = true return Board(id) } - val boardEntityClass = object : IntEntityClass(Boards, entityCtor = ::createBoard) { } + + val boardEntityClass = object : IntEntityClass(Boards, entityCtor = ::createBoard) {} withTables(Boards) { val board = boardEntityClass.new { @@ -1371,8 +1395,7 @@ class EntityTests : DatabaseTestsBase() { assertEquals("Test Board", board.name) assertTrue( - createBoardCalled, - "Expected createBoardCalled to be called" + createBoardCalled, "Expected createBoardCalled to be called" ) } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/MathFunctionTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/MathFunctionTests.kt index 96964bce9c..56a1171489 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/MathFunctionTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/MathFunctionTests.kt @@ -1,6 +1,5 @@ package org.jetbrains.exposed.sql.tests.shared.functions -import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.functions.math.* import org.jetbrains.exposed.sql.tests.TestDB @@ -68,18 +67,36 @@ class MathFunctionTests : FunctionsTestBase() { assertExpressionEqual(BigDecimal(100), PowerFunction(intLiteral(10), doubleLiteral(2.0))) if (testDb != TestDB.SQLSERVER) { assertExpressionEqual(BigDecimal("102.01"), PowerFunction(doubleLiteral(10.1), intLiteral(2))) - assertExpressionEqual(BigDecimal("102.01"), PowerFunction(decimalLiteral(BigDecimal("10.1")), intLiteral(2))) + assertExpressionEqual( + BigDecimal("102.01"), + PowerFunction(decimalLiteral(BigDecimal("10.1")), intLiteral(2)) + ) assertExpressionEqual(BigDecimal("102.01"), PowerFunction(doubleLiteral(10.1), doubleLiteral(2.0))) - assertExpressionEqual(BigDecimal("102.01"), PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.0))) - assertExpressionEqual(BigDecimal("324.1928515714"), PowerFunction(doubleLiteral(10.1), doubleLiteral(2.5))) - assertExpressionEqual(BigDecimal("324.1928515714"), PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.5))) + assertExpressionEqual( + BigDecimal("102.01"), + PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.0)) + ) + assertExpressionEqual( + BigDecimal("324.1928515714"), + PowerFunction(doubleLiteral(10.1), doubleLiteral(2.5)) + ) + assertExpressionEqual( + BigDecimal("324.1928515714"), + PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.5)) + ) } else { assertExpressionEqual(BigDecimal(102), PowerFunction(doubleLiteral(10.1), intLiteral(2))) assertExpressionEqual(BigDecimal(102), PowerFunction(decimalLiteral(BigDecimal("10.1")), intLiteral(2))) assertExpressionEqual(BigDecimal(102), PowerFunction(doubleLiteral(10.1), doubleLiteral(2.0))) - assertExpressionEqual(BigDecimal(102), PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.0))) + assertExpressionEqual( + BigDecimal(102), + PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.0)) + ) assertExpressionEqual(BigDecimal("324.2"), PowerFunction(doubleLiteral(10.1), doubleLiteral(2.5))) - assertExpressionEqual(BigDecimal("324.2"), PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.5))) + assertExpressionEqual( + BigDecimal("324.2"), + PowerFunction(decimalLiteral(BigDecimal("10.1")), doubleLiteral(2.5)) + ) } } } @@ -105,23 +122,11 @@ class MathFunctionTests : FunctionsTestBase() { assertExpressionEqual(BigDecimal("11.2"), SqrtFunction(decimalLiteral(BigDecimal("125.44")))) when (testDb) { - TestDB.MYSQL, TestDB.MARIADB -> { + TestDB.MYSQL, TestDB.MARIADB, TestDB.SQLITE -> { assertExpressionEqual(null, SqrtFunction(intLiteral(-100))) } - TestDB.SQLSERVER -> { - // SQLServer fails with SQLServerException to execute sqrt with negative value - expectException { - assertExpressionEqual(null, SqrtFunction(intLiteral(-100))) - } - } - TestDB.SQLITE, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.ORACLE -> { - // SQLite, PSQL, Oracle fail to execute sqrt with negative value - expectException { - assertExpressionEqual(null, SqrtFunction(intLiteral(-100))) - } - } else -> { - expectException { + expectException { assertExpressionEqual(null, SqrtFunction(intLiteral(-100))) } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt index b90b8d9722..1ded40692c 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt @@ -28,7 +28,7 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() { @Test fun `test ON DELETE SET DEFAULT for SQLite`() { - Assume.assumeTrue(TestDB.SQLITE in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.SQLITE in TestDB.enabledDialects()) transaction(Database.connect("jdbc:sqlite:file:test?mode=memory&cache=shared&foreign_keys=on", user = "root", driver = "org.sqlite.JDBC")) { testOnDeleteSetDefault() @@ -36,6 +36,7 @@ class ForeignKeyConstraintTests : DatabaseTestsBase() { } private fun Transaction.testOnDeleteSetDefault() { + SchemaUtils.drop(Category, Item) SchemaUtils.create(Category, Item) Category.insert { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/MultipleDatabaseBugTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/MultipleDatabaseBugTest.kt index 2e459ba65a..0690cc6242 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/MultipleDatabaseBugTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/MultipleDatabaseBugTest.kt @@ -36,7 +36,7 @@ class MultipleDatabaseBugTest { @Before fun before() { - Assume.assumeTrue(TestDB.SQLITE in TestDB.enabledInTests()) + Assume.assumeTrue(TestDB.SQLITE in TestDB.enabledDialects()) val filename = folder.newFile("foo.db").absolutePath val ds = SQLiteDataSource() ds.url = "jdbc:sqlite:" + filename diff --git a/gradle.properties b/gradle.properties index 99b65eabec..f6d4415a94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,7 @@ org.gradle.parallel=false org.gradle.jvmargs=-Dfile.encoding=UTF-8 -# +org.gradle.configuration.cache=true +org.gradle.caching=true + group=org.jetbrains.exposed version=0.43.0 diff --git a/samples/exposed-ktor/build.gradle.kts b/samples/exposed-ktor/build.gradle.kts index 749d2998ee..923d7d291d 100644 --- a/samples/exposed-ktor/build.gradle.kts +++ b/samples/exposed-ktor/build.gradle.kts @@ -5,9 +5,9 @@ val exposedVersion: String by project val h2Version: String by project plugins { - kotlin("jvm") version "1.8.21" - id("io.ktor.plugin") version "2.3.1" - id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" + kotlin("jvm") version "1.9.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" + id("io.ktor.plugin") version "2.3.4" } group = "org.jetbrains.exposed.samples.ktor" diff --git a/samples/exposed-ktor/gradle.properties b/samples/exposed-ktor/gradle.properties index 62fe6cec82..9545e773b1 100644 --- a/samples/exposed-ktor/gradle.properties +++ b/samples/exposed-ktor/gradle.properties @@ -1,5 +1,5 @@ -ktorVersion=2.3.1 -kotlinVersion=1.8.21 +ktorVersion=2.3.4 +kotlinVersion=1.8.10 logbackVersion=1.2.11 kotlin.code.style=official exposedVersion=0.43.0