diff --git a/.editorconfig b/.editorconfig index e28d8216da..0fa7fc2648 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,9 +6,9 @@ end_of_line = lf indent_size = tab indent_style = space insert_final_newline = true -max_line_length = 150 +max_line_length = 166 tab_width = 4 -ij_formatter_off_tag = @formatter:off +ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on ij_formatter_tags_enabled = true ij_smart_tabs = true @@ -41,6 +41,5 @@ ij_kotlin_spaces_around_equality_operators = true ij_any_align_group_field_declarations = false ij_java_align_group_field_declarations = false - [{.github/**/*.yml, .idea/*.xml}] indent_size = 2 diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml new file mode 100644 index 0000000000..19d77e3c3a --- /dev/null +++ b/.github/workflows/detekt.yml @@ -0,0 +1,57 @@ +# This workflow performs a static analysis of your Kotlin source code using detekt. +# +# Scans are triggered on every pull request targeting the main branch. + +name: detekt + +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + + # Triggers the workflow on pull request events targeting the main branch + pull_request: + branches: + - main + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "detekt" + detekt: + # The type of runner that the job will run on + runs-on: ubuntu-latest + steps: + # Checks out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: "checkout" + uses: actions/checkout@v2 + # Determines changed files + - name: "changed-files" + run: | + git fetch origin main:refs/remotes/origin/main + CHANGED_FILES=$(git --no-pager diff --name-only --diff-filter=dr origin/main HEAD -- . ':!exposed-tests' | grep '\(.*.kt\|.*.kts\)$' | tr '\n' , | rev | cut -c 2- | rev) + echo "Changed files: $CHANGED_FILES" + echo "CHANGED_FILES=$CHANGED_FILES" >> "$GITHUB_ENV" + # Resolves and installs a specific version of detekt on to a GitHub Actions Runner if not already present in the tool cache + - name: "set-up-detekt" + uses: peter-murray/setup-detekt@v2 + with: + detekt_version: 1.23.1 + # Runs detekt on changed files + - name: "run-detekt" + run: | + if [ -z "$CHANGED_FILES" ] + then + echo "No changed Kotlin files found." + exit 0 + fi + + echo "Running detekt check..." + DETEKT_ISSUES=$(detekt-cli --build-upon-default-config --config detekt/detekt-config.yml --plugins detekt/detekt-formatting-1.23.1.jar --input $CHANGED_FILES) + if [ -n "$DETEKT_ISSUES" ]; then + echo "***********************************************" + echo "$DETEKT_ISSUES" + echo "***********************************************" + echo " detekt failed " + echo " Please fix the above issues " + echo "***********************************************" + exit 1 + fi diff --git a/.idea/detekt.xml b/.idea/detekt.xml index 62877118cb..354d38c4b8 100644 --- a/.idea/detekt.xml +++ b/.idea/detekt.xml @@ -1,9 +1,20 @@ - - true - true - true - $PROJECT_DIR$/detekt.yml - - \ No newline at end of file + + true + true + true + $PROJECT_DIR$/detekt/detekt-config.yml + + + + + + diff --git a/docs/README.md b/README.md similarity index 63% rename from docs/README.md rename to README.md index 518150d1e7..c7a8eefb47 100644 --- a/docs/README.md +++ b/README.md @@ -1,10 +1,10 @@
-Exposed
+Exposed

[![JetBrains team project](https://jb.gg/badges/team.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![Kotlinlang Slack Channel](https://img.shields.io/badge/slack-@kotlinlang/exposed-yellow.svg?logo=slack?style=flat)](https://kotlinlang.slack.com/archives/C0CG7E0A1) -[![TC Build status](https://teamcity.jetbrains.com/app/rest/builds/buildType:(id:KotlinTools_Exposed_Build)/statusIcon)](https://teamcity.jetbrains.com/viewType.html?buildTypeId=KotlinTools_Exposed_Build&guest=1) +[![TC Build status](https://exposed.teamcity.com/app/rest/builds/buildType:id:Exposed_Build/statusIcon.svg)](https://exposed.teamcity.com/viewType.html?buildTypeId=Exposed_Build&guest=1) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.jetbrains.exposed/exposed-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.jetbrains.exposed/exposed-core) [![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) @@ -13,18 +13,179 @@ Exposed is a lightweight SQL library on top of JDBC driver for Kotlin language. Exposed has two flavors of database access: typesafe SQL wrapping DSL and lightweight Data Access Objects (DAO). -With Exposed you can have two levels of databases Access. You would like to use exposed because the database access includes wrapping DSL and a lightweight data access object. Also, our official mascot is Cuttlefish, which is well known for its outstanding mimicry ability that enables it to blend seamlessly in any environment. +With Exposed, you have two ways for database access: wrapping DSL and a lightweight DAO. Our official mascot is the cuttlefish, which is well-known for its outstanding mimicry ability that enables it to blend seamlessly into any environment. Similar to our mascot, Exposed can be used to mimic a variety of database engines and help you build applications without dependencies on any specific database engine and switch between them with very little or no changes. ## Supported Databases -- ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) (Also, PostgreSQL using the [pgjdbc-ng](https://github.com/impossibl/pgjdbc-ng) JDBC driver) -- ![MySQL](https://img.shields.io/badge/mysql-%2300f.svg?style=for-the-badge&logo=mysql&logoColor=white) -- ![MariaDB](https://img.shields.io/badge/MariaDB-003545?style=for-the-badge&logo=mariadb&logoColor=white) -- ![SQLite](https://img.shields.io/badge/sqlite-%2307405e.svg?style=for-the-badge&logo=sqlite&logoColor=white) -- H2 (versions 2.x; 1.x version is deprecated and will be removed in future releases) -- [Oracle](ORACLE.md) -- [SQL Server](SQLServer.md) +- H2 (versions 2.x; 1.x version is deprecated and will be removed in future releases) +- ![MariaDB](https://img.shields.io/badge/MariaDB-003545?style=for-the-badge&logo=mariadb&logoColor=white) +- ![MySQL](https://img.shields.io/badge/mysql-%2300f.svg?style=for-the-badge&logo=mysql&logoColor=white) +- [Oracle](docs/ORACLE.md) +- ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) (Also, PostgreSQL using + the [pgjdbc-ng](https://github.com/impossibl/pgjdbc-ng) JDBC driver) +- [SQL Server](docs/SQLServer.md) +- ![SQLite](https://img.shields.io/badge/sqlite-%2307405e.svg?style=for-the-badge&logo=sqlite&logoColor=white) + +## Dependencies + +### Maven Central configuration + +Releases of Exposed are available in the Maven Central repository. You can declare this repository in your build script as follows: + +#### Maven + +```xml + + + + + mavenCentral + mavenCentral + https://repo1.maven.org/maven2/ + + +``` + +#### Gradle Groovy and Kotlin DSL + +**Warning:** You might need to set your Kotlin JVM target to 8 and when using Spring to 17 in order for it to work properly + +```kotlin +repositories { + // Versions after 0.30.1 + // Versions before 0.30.1 is unavailable for now + mavenCentral() +} +``` + +### Exposed modules + +`Exposed` consists of the following modules: + +* exposed-core - base module, which contains both DSL api along with mapping +* exposed-crypt - provides additional column types to store encrypted data in DB and encode/decode it on client-side +* exposed-dao - DAO api +* exposed-java-time - date-time extensions based on Java8 Time API +* exposed-jdbc - transport level implementation based on Java JDBC API +* exposed-jodatime - date-time extensions based on JodaTime library +* exposed-json - JSON and JSONB data type extensions +* exposed-kotlin-datetime - date-time extensions based on kotlinx-datetime +* exposed-money - extensions to support MonetaryAmount from "javax.money:money-api" +* exposed-spring-boot-starter - a starter for [Spring Boot](https://spring.io/projects/spring-boot) to utilize Exposed as the ORM instead + of [Hibernate](https://hibernate.org/) + +```xml + + + + org.jetbrains.exposed + exposed-core + 0.44.0 + + + org.jetbrains.exposed + exposed-crypt + 0.44.0 + + + org.jetbrains.exposed + exposed-dao + 0.44.0 + + + org.jetbrains.exposed + exposed-java-time + 0.44.0 + + + org.jetbrains.exposed + exposed-jdbc + 0.44.0 + + + org.jetbrains.exposed + exposed-jodatime + 0.44.0 + + + org.jetbrains.exposed + exposed-json + 0.44.0 + + + org.jetbrains.exposed + exposed-kotlin-datetime + 0.44.0 + + + org.jetbrains.exposed + exposed-money + 0.44.0 + + + org.jetbrains.exposed + exposed-spring-boot-starter + 0.44.0 + + + +``` + +#### Gradle Groovy + +```groovy +dependencies { + implementation 'org.jetbrains.exposed:exposed-core:0.44.0' + implementation 'org.jetbrains.exposed:exposed-crypt:0.44.0' + implementation 'org.jetbrains.exposed:exposed-dao:0.44.0' + implementation 'org.jetbrains.exposed:exposed-jdbc:0.44.0' + + implementation 'org.jetbrains.exposed:exposed-jodatime:0.44.0' + // or + implementation 'org.jetbrains.exposed:exposed-java-time:0.44.0' + // or + implementation 'org.jetbrains.exposed:exposed-kotlin-datetime:0.44.0' + + implementation 'org.jetbrains.exposed:exposed-json:0.44.0' + implementation 'org.jetbrains.exposed:exposed-money:0.44.0' + implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0' +} +``` + +#### Gradle Kotlin DSL + +In `build.gradle.kts`: + +```kotlin +val exposedVersion: String by project +dependencies { + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-crypt:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") + + implementation("org.jetbrains.exposed:exposed-jodatime:$exposedVersion") + // or + implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") + // or + implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion") + + implementation("org.jetbrains.exposed:exposed-json:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-money:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion") +} +``` + +and in `gradle.properties` + +``` +exposedVersion=0.44.0 +``` + +## Samples + +Check out the [samples](samples/README.md) for a quick start. ## Links @@ -33,15 +194,16 @@ Currently, Exposed is available for **maven/gradle builds**. Check the [Maven Ce For more information visit the links below: - [Wiki](https://github.com/JetBrains/Exposed/wiki) with examples and docs -- [Roadmap](ROADMAP.md) to see what's coming next -- [Change log](ChangeLog.md) of improvements and bug fixes +- [Roadmap](docs/ROADMAP.md) to see what's coming next +- [Change log](docs/ChangeLog.md) of improvements and bug fixes +- [Breaking changes](docs/BREAKING_CHANGES.md) and any migration details - [Slack Channel](https://kotlinlang.slack.com/archives/C0CG7E0A1) - [Issue Tracker](https://youtrack.jetbrains.com/issues/EXPOSED)

## Filing issues -Please note that we are moving away from GitHub Issues for reporting of bugs and features. Please log any new requests on [YouTrack](https://youtrack.jetbrains.com/issues/EXPOSED). +Please note that we are moving away from GitHub Issues for reporting of bugs and features. Please log any new requests on [YouTrack](https://youtrack.jetbrains.com/issues/EXPOSED). You must be logged in to view and log issues, otherwise you will be met with a 404. ## Community diff --git a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/DockerTestContainers.kt b/api/exposed.api similarity index 100% rename from buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/DockerTestContainers.kt rename to api/exposed.api diff --git a/build.gradle.kts b/build.gradle.kts index 7e4d425166..2c074999a9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,36 +1,50 @@ 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() } } +apiValidation { + ignoredProjects.addAll(listOf("exposed-tests", "exposed-bom")) +} + val reportMerge by tasks.registering(ReportMergeTask::class) { output.set(rootProject.buildDir.resolve("reports/detekt/exposed.xml")) } subprojects { dependencies { - detektPlugins("io.gitlab.arturbosch.detekt", "detekt-formatting", "1.21.0") + detektPlugins("io.gitlab.arturbosch.detekt", "detekt-formatting", "1.23.1") } 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" @@ -40,17 +54,104 @@ subprojects { } } -nexusPublishing { - repositories { - sonatype { - username.set(System.getenv("exposed.sonatype.user")) - password.set(System.getenv("exposed.sonatype.password")) - useStaging.set(true) +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}") } } -} -repositories { - mavenLocal() - mavenCentral() + 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 2692f88e03..1ecf490e57 100644 --- a/buildScripts/docker/docker-compose-sqlserver.yml +++ b/buildScripts/docker/docker-compose-sqlserver.yml @@ -1,12 +1,12 @@ version: '3.1' services: - sqlserver: - container_name: SQLServer - image: mcr.microsoft.com/mssql/server:2017-latest - ports: - - "1433" - environment: - ACCEPT_EULA: "Y" - SA_PASSWORD: "yourStrong(!)Password" - MSSQL_PID: "Express" + sqlserver: + container_name: SQLServer + restart: always + image: mcr.microsoft.com/azure-sql-edge:1.0.7 + ports: + - "3005:1433" + environment: + ACCEPT_EULA: "1" + SA_PASSWORD: "Exposed_password_1!" diff --git a/buildScripts/gradle/checkstyle.gradle.kts b/buildScripts/gradle/checkstyle.gradle.kts deleted file mode 100644 index 2793fa290d..0000000000 --- a/buildScripts/gradle/checkstyle.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -import io.gitlab.arturbosch.detekt.extensions.DetektExtension -import io.gitlab.arturbosch.detekt.DetektPlugin - -apply() - -configure { - ignoreFailures = false - buildUponDefaultConfig = true - config = files( - rootDir.resolve("detekt.yml").takeIf { it.isFile }, - projectDir.resolve("detekt.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 5013e67558..0000000000 --- a/buildScripts/gradle/publishing.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -import org.jetbrains.exposed.gradle.* - -apply(plugin = "java-library") -apply(plugin = "maven-publish") -apply(plugin = "signing") - -_java { - withJavadocJar() - withSourcesJar() -} - -_publishing { - publications { - create("ExposedJars") { - artifactId = project.name - from(project.components["java"]) - pom { - configureMavenCentralMetadata(project) - } - signPublicationIfKeyPresent(project) - } - } -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0d97fa775c..a12ae60935 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -5,10 +5,9 @@ repositories { dependencies { gradleApi() - implementation("org.jetbrains.kotlin.jvm", "org.jetbrains.kotlin.jvm.gradle.plugin", "1.7.21") - implementation("com.avast.gradle", "gradle-docker-compose-plugin", "0.14.9") - implementation("io.github.gradle-nexus", "publish-plugin", "1.0.0") - implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.21.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.23.1") } plugins { 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 f2889641ec..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Accessors.kt +++ /dev/null @@ -1,12 +0,0 @@ -@file:Suppress("FunctionName") - -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 c3306e1bcb..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/DBTestingPlugin.kt +++ /dev/null @@ -1,115 +0,0 @@ -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 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, - mysql51, - postgres, - postgresNG - ) - } - } - } -} - -/** - * 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 bfc5d540eb..1b784e7ad4 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Publishing.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Publishing.kt @@ -1,14 +1,16 @@ -@file:Suppress("UnstableApiUsage") - package org.jetbrains.exposed.gradle import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension 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 -import org.gradle.api.plugins.JavaPluginExtension infix fun Property.by(value: T) { set(value) @@ -55,10 +57,54 @@ fun MavenPublication.signPublicationIfKeyPresent(project: Project) { } } -fun Project._publishing(configure: PublishingExtension.() -> Unit) { +fun Project.publishing(configure: PublishingExtension.() -> Unit) { extensions.configure("publishing", configure) } -fun Project._java(configure: JavaPluginExtension.() -> Unit) { +fun Project.java(configure: JavaPluginExtension.() -> Unit) { extensions.configure("java", configure) -} \ No newline at end of file +} + +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 fafc243740..22d65dfa28 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Versions.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/Versions.kt @@ -1,29 +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.43.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 = "6.0.11" + const val springBoot = "3.1.3" } 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 ebcfc3b3d3..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTest.kt +++ /dev/null @@ -1,48 +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 50de0ce679..0000000000 --- a/buildSrc/src/main/kotlin/org/jetbrains/exposed/gradle/tasks/DBTestWithDockerCompose.kt +++ /dev/null @@ -1,47 +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) - 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/detekt.yml b/detekt/detekt-config.yml similarity index 65% rename from detekt.yml rename to detekt/detekt-config.yml index 2443da807f..b7d0727410 100644 --- a/detekt.yml +++ b/detekt/detekt-config.yml @@ -4,6 +4,10 @@ build: maxIssues: 52 formatting: + ArgumentListWrapping: + active: false + EnumEntryNameCase: + active: false MaximumLineLength: maxLineLength: 166 MultiLineIfElse: @@ -13,11 +17,20 @@ formatting: active: true autoCorrect: true indentSize: 4 + maxLineLength: 166 NoWildcardImports: active: false NoMultipleSpaces: active: true autoCorrect: false + PropertyWrapping: + active: false + SpacingAroundUnaryOperator: + active: true + autoCorrect: true + Wrapping: + active: true + maxLineLength: 166 style: MaxLineLength: @@ -28,7 +41,10 @@ style: WildcardImport: active: false MagicNumber: + ignoreNamedArgument: true ignoreRanges: true + ThrowsCount: + active: false complexity: LongParameterList: @@ -40,5 +56,11 @@ complexity: thresholdInClasses: 40 thresholdInFiles: 100 thresholdInObjects: 26 - ComplexMethod: + CyclomaticComplexMethod: threshold: 26 + LongMethod: + threshold: 90 + +naming: + ConstructorParameterNaming: + active: false diff --git a/detekt/detekt-formatting-1.23.1.jar b/detekt/detekt-formatting-1.23.1.jar new file mode 100644 index 0000000000..3c2d34bc59 Binary files /dev/null and b/detekt/detekt-formatting-1.23.1.jar differ diff --git a/docs/BREAKING_CHANGES.md b/docs/BREAKING_CHANGES.md new file mode 100644 index 0000000000..f8e7c3fcf8 --- /dev/null +++ b/docs/BREAKING_CHANGES.md @@ -0,0 +1,103 @@ +# Breaking Changes + +## 0.44.0 + +* `SpringTransactionManager` no longer extends `DataSourceTransactionManager`; instead, it directly extends `AbstractPlatformTransactionManager` while retaining the previous basic functionality. + The class also no longer implements the Exposed interface `TransactionManager`, as transaction operations are instead delegated to Spring. + These changes ensure that Exposed's underlying transaction management no longer interferes with the expected behavior of Spring's transaction management, for example, + when using nested transactions or with `@Transactional` elements like `propagation` or `isolation`. + + If integration still requires a `DataSourceTransactionManager`, please add two bean declarations to the configuration: one for `SpringTransactionManager` and one for `DataSourceTransactionManager`. + Then define a composite transaction manager that combines these two managers. + + If `TransactionManager` functions were being invoked by a `SpringTransactionManager` instance, please replace these calls with the appropriate Spring annotation + or, if necessary, by using the companion object of `TransactionManager` directly (for example, `TransactionManager.currentOrNull()`). +* `spring-transaction` and `exposed-spring-boot-starter` modules now use Spring Framework 6.0 and Spring Boot 3.0, which require Java 17 as a minimum version. +* A table that is created with a keyword identifier (a table or column name) now logs a warning that the identifier's case may be lost when it is automatically quoted in generated SQL. + This primarily affects H2 and Oracle, both of which support folding identifiers to uppercase, and PostgreSQL, which folds identifiers to lower case. + + To remove these warnings and to ensure that the keyword identifier sent to the database matches the exact case used in the Exposed table object, please set `preserveKeywordCasing = true` in `DatabaseConfig`: +```kotlin +object TestTable : Table("table") { + val col = integer("select") +} + +// without opt-in (default set to false) +// H2 generates SQL -> CREATE TABLE IF NOT EXISTS "TABLE" ("SELECT" INT NOT NULL) + +// with opt-in +Database.connect( + url = "jdbc:h2:mem:test", + driver = "org.h2.Driver", + databaseConfig = DatabaseConfig { + @OptIn(ExperimentalKeywordApi::class) + preserveKeywordCasing = true + } +) +// H2 generates SQL -> CREATE TABLE IF NOT EXISTS "table" ("select" INT NOT NULL) +``` + +**Note:** `preserveKeywordCasing` is an experimental flag and requires `@OptIn`. It may become deprecated in future releases and its behavior when set to `true` may become the default. + +## 0.43.0 + +* In all databases except MySQL, MariaDB, and SQL Server, the `ubyte()` column now maps to data type `SMALLINT` instead of `TINYINT`, which allows the full range of +`UByte` values to be inserted without any overflow. +Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and `UByte.MAX_VALUE`. +If a column that only uses 1 byte of storage is needed, but without allowing any non-negative values to be inserted, please use a signed `byte()` column +instead with a manually created check constraint: +```kotlin +byte("number").check { it.between(0, Byte.MAX_VALUE) } +// OR +byte("number").check { (it greaterEq 0) and (it lessEq Byte.MAX_VALUE) } +``` +* In all databases except MySQL and MariaDB, the `uint()` column now maps to data type `BIGINT` instead of `INT`, which allows the full range of `UInt` values to +be inserted without any overflow. +Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and `UInt.MAX_VALUE`. +If a column that only uses 4 bytes of storage is needed, but without allowing any non-negative values to be inserted, please use a signed `integer()` column +instead with a manually created check constraint: +```kotlin +integer("number").check { it.between(0, Int.MAX_VALUE) } +// OR +integer("number").check { (it greaterEq 0) and (it lessEq Int.MAX_VALUE) } +``` + +## 0.42.0 + +* [SQLite] The table column created using `date()` now uses TEXT datatype instead of DATE (which the database mapped internally to NUMERIC type). +This applies to the specific `DateColumnType` in all 3 date/time modules and means `LocalDate` comparisons can now be done directly without conversions. +* [H2, PostgreSQL] Using `replace()` now throws an exception as the REPLACE command is not supported by these databases. + If `replace()` was being used to perform an insert or update operation, all usages should instead be switched to `upsert()`. +[See wiki for UPSERT details](https://github.com/JetBrains/Exposed/wiki/DSL#insert-or-update) +* Operator classes `exists` and `notExists` have been renamed to `Exists` and `NotExists`. +The functions `exists()` and `notExists()` have been introduced to return an instance of their respectively-named classes and to avoid unresolved reference issues. +Any usages of these classes should be renamed to their capitalized forms. +* `customEnumeration()` now registers a `CustomEnumerationColumnType` to allow referencing by another column. +The signature of `customEnumeration()` has not changed and table columns initialized using it are still of type `Column`. +* `Transaction.suspendedTransaction()` has been renamed to `Transaction.withSuspendTransaction()`. +Please run `Edit -> Find -> Replace in files...` twice with `suspendedTransaction(` and `suspendedTransaction ` as the search options, +to ensure that both variants are replaced without affecting `suspendedTransactionAsync()` (if used in code). +* The `repetitionAttempts` parameter in `transaction()` has been removed and replaced with a mutable property in the `Transaction` class. +Please remove any arguments for this parameter and assign values to the property directly: +```kotlin +// before +transaction(Connection.TRANSACTION_READ_COMMITTED, repetitionAttempts = 10) { + // statements +} + +// after +transaction(Connection.TRANSACTION_READ_COMMITTED) { + repetitionAttempts = 10 + // statements +} +``` +* In all databases except MySQL and MariaDB, the `ushort()` column now maps to data type `INT` instead of `SMALLINT`, which allows the full range of `UShort` +values to be inserted without any overflow. +Registering the column on a table also creates a check constraint that restricts inserted data to the range between 0 and `UShort.MAX_VALUE`. +If a column that only uses 2 bytes of storage is needed, but without allowing any non-negative values to be inserted, please use a signed `short()` column +instead with a manually created check constraint: +```kotlin +short("number").check { it.between(0, Short.MAX_VALUE) } +// OR +short("number").check { (it greaterEq 0) and (it lessEq Short.MAX_VALUE) } +``` diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3edbda0e67..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 @@ -30,6 +54,8 @@ Contributions are made using Github [pull requests](https://help.github.com/en/a 7. If the contribution requires updates to documentation (be it updating existing contents or creating new one), please file a new ticket on [YouTrack](https://youtrack.jetbrains.com/issues/EXPOSED). 8. Make sure any code contributed is covered by tests and no existing tests are broken. We use Docker containers to run tests. +9. Finally, make sure to run the `apiCheck` Gradle task. If it's not successful, run the `apiDump` Gradle task. Further information can be + found [here](https://github.com/Kotlin/binary-compatibility-validator). #### Style Guides diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index da09d9aebf..0630a53d61 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,3 +1,155 @@ +# 0.44.0 +Infrastructure: +* Kotlin 1.9.10 +* Kotlin Coroutines 1.7.3 +* log4j2 2.20.0 +* h2-database driver 2.2.220 +* MariaDB driver 2.7.9 and 3.1.4 +* MySQL driver 8.0.30 +* PostgreSQL driver 42.6.0 +* SQLite driver 3.43.0.0 +* Spring Framework 6.0.11 +* Spring Boot 3.1.3 + +Breaking changes: +* `SpringTransactionManager` no longer extends `DataSourceTransactionManager`; instead, it directly extends `AbstractPlatformTransactionManager`. + The class also no longer implements the Exposed interface `TransactionManager`, as transaction operations are instead delegated to Spring. +* `spring-transaction` and `exposed-spring-boot-starter` modules now use Spring Framework 6.0 and Spring Boot 3.0, which require Java 17 as a minimum version. +* A table that is created with a keyword identifier now logs a warning that the identifier's case may be lost when it is automatically quoted in generated SQL. + `DatabaseConfig` now includes the property `preserveKeywordCasing`, which can be set to `true` to remove these warnings and to ensure that the identifier matches the exact case used. +* More details at [Breaking changes](BREAKING_CHANGES.md#0440) + +Features: +* feat!: EXPOSED-109 Improve implementation of Spring transaction manager by @FullOfOrange in https://github.com/JetBrains/Exposed/pull/1840 +* feat: EXPOSED-78 Support database-generated values for columns by @joc-a in https://github.com/JetBrains/Exposed/pull/1844 +* feat: EXPOSED-188 Support Propagation in SpringTransactionManager by @FullOfOrange in https://github.com/JetBrains/Exposed/pull/1867 + +Bug fixes: +* docs: EXPOSED-159 Add KDocs for EntityClass reference functions by @bog-walk in https://github.com/JetBrains/Exposed/pull/1848 +* fix: EXPOSED-158 avoid SQL syntax error of CASE WHEN using nested CASE by @ymotchi in https://github.com/JetBrains/Exposed/pull/1847 +* Fix how changes are calculated for non-default schema table by @AlexeySoshin in https://github.com/JetBrains/Exposed/pull/1678 +* fix: Fix tables creation depending on each other via foreignKey constraint by @naftalmm in https://github.com/JetBrains/Exposed/pull/1649 +* fix: Verbose logging in test module by @Hakky54 in https://github.com/JetBrains/Exposed/pull/1852 +* fix: EXPOSED-161 SQL Server syntax incorrectly allows CASCADE with dropSchema by @bog-walk in https://github.com/JetBrains/Exposed/pull/1850 +* chore: Reuse Containers in tests; Add Test parameters by @e5l in https://github.com/JetBrains/Exposed/pull/1853 +* docs: EXPOSED-124 Add Spring Boot samples by @FullOfOrange in https://github.com/JetBrains/Exposed/pull/1826 +* fix: EXPOSED-117 Set jvmToolchain to 8 for all modules by @e5l in https://github.com/JetBrains/Exposed/pull/1855 +* chore: Adjust test md files by @joc-a in https://github.com/JetBrains/Exposed/pull/1857 +* fix: EXPOSED-162 SQLite generatedKeys exception by @joc-a in https://github.com/JetBrains/Exposed/pull/1854 +* fix: Unable to download required toolchain on MAC by @joc-a in https://github.com/JetBrains/Exposed/pull/1859 +* fix: EXPOSED-171 Switch from spring.factories to AutoConfiguration.imports by @rbraeunlich in https://github.com/JetBrains/Exposed/pull/1645 +* fix: EXPOSED-179 Unsigned column check constraint is not unique to table by @bog-walk in https://github.com/JetBrains/Exposed/pull/1860 +* fix: EXPOSED-182 Schema name breaks Create Table with default column in SQLServer by @bog-walk in https://github.com/JetBrains/Exposed/pull/1861 +* fix: Exception when using RESTRICT reference option by @joc-a in https://github.com/JetBrains/Exposed/pull/1862 +* fix!: EXPOSED-150 Auto-quoted column names change case across databases by @bog-walk in https://github.com/JetBrains/Exposed/pull/1841 +* docs: EXPOSED-132 Add annotations to spring-boot-starter README samples by @bog-walk in https://github.com/JetBrains/Exposed/pull/1856 +* fix: EXPOSED-173 UPDATE_RULE read incorrectly for Oracle by @joc-a in https://github.com/JetBrains/Exposed/pull/1865 +* chore: EXPOSED-186 Replace JDK 1.7 support in exposed-jodatime classes by @bog-walk in https://github.com/JetBrains/Exposed/pull/1866 +* fix: EXPOSED-178 DELETE_RULE read incorrectly for Oracle by @joc-a in https://github.com/JetBrains/Exposed/pull/1868 + +# 0.43.0 +Infrastructure: +* Kotlin 1.9.10 + +Features: +* feat: EXPOSED-85 Add support for changing default value in SQL Server by @joc-a in https://github.com/JetBrains/Exposed/pull/1812 + +Bug fixes: +* fix: EXPOSED-107 Inaccurate UByte column type mapping by @bog-walk in https://github.com/JetBrains/Exposed/pull/1808 +* fix: EXPOSED-108 Incorrect mapping for UInt data type by @bog-walk in https://github.com/JetBrains/Exposed/pull/1809 +* fix: Inaccurate drop database statement in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1807 +* test: Fix failing datetime comparison tests in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1813 +* fix: EXPOSED-111 Allow check constraint statements in MySQL8 by @bog-walk in https://github.com/JetBrains/Exposed/pull/1817 +* fix: EXPOSED-116 UUID conversion error with upsert in H2 by @bog-walk in https://github.com/JetBrains/Exposed/pull/1823 +* fix: EXPOSED-112 SchemaUtils fails to compare default CURRENT_TIMESTAMP by @bog-walk in https://github.com/JetBrains/Exposed/pull/1819 +* fix: EXPOSED-123 ExposedBlob.getBytes() fails on Oracle with IOException by @bog-walk in https://github.com/JetBrains/Exposed/pull/1824 +* fix: EXPOSED-128 Update with Join and Where clause fails in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1825 +* fix: EXPOSED-135 Oracle does not use setSchema value as currentScheme by @bog-walk in https://github.com/JetBrains/Exposed/pull/1828 +* fix: EXPOSED-122 Fix timestampWithTimeZone tests in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1829 +* fix: EXPOSED-137 SET DEFAULT reference option should not be supported in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1830 +* test: Fix failing Oracle tests in exposed-tests by @bog-walk in https://github.com/JetBrains/Exposed/pull/1831 +* fix: EXPOSED-127 Default values for JSON columns are not quoted by @bog-walk in https://github.com/JetBrains/Exposed/pull/1827 +* fix: EXPOSED-145 Quoted table name breaks CREATE SEQUENCE in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1836 +* fix: EXPOSED-130 Logger throws ClassCastException with JSON and ListSerializer by @bog-walk in https://github.com/JetBrains/Exposed/pull/1835 +* fix: EXPOSED-133 Suspend transactions blocking Hikari connection pool by @bog-walk in https://github.com/JetBrains/Exposed/pull/1837 +* fix: EXPOSED-151 Quoted identifiers cause incorrect schema validation by @bog-walk in https://github.com/JetBrains/Exposed/pull/1842 +* fix: Remove false warning log by @joc-a in https://github.com/JetBrains/Exposed/pull/1843 + +Breaking changes: +* [Breaking changes](BREAKING_CHANGES.md) + +# 0.42.1 +Infrastructure: +* Kotlin 1.9.0 + +Bug fixes: +* fix: exposed-bom module missing when publishing by @joc-a in https://github.com/JetBrains/Exposed/pull/1818 + +# 0.42.0 +Infrastructure: +* Kotlin 1.9.0 + +Deprecations: +* deprecate: EXPOSED-84 Raise deprecation levels of API elements by @bog-walk in https://github.com/JetBrains/Exposed/pull/1771 + +Breaking changes: +* [Breaking changes](BREAKING_CHANGES.md) + +Features: +* Add CHARINDEX function for sqlserver by @eukleshnin in https://github.com/JetBrains/Exposed/pull/1675 +* feat: EXPOSED-32 Support string function CHAR_LENGTH by @bog-walk in https://github.com/JetBrains/Exposed/pull/1737 +* feat: EXPOSED-37 Support null-safe equality comparison by @bog-walk in https://github.com/JetBrains/Exposed/pull/1739 +* feat: EXPOSED-45 Support single statement UPSERT by @bog-walk in https://github.com/JetBrains/Exposed/pull/1743 +* feat: EXPOSED-52 Support batch UPSERT by @bog-walk in https://github.com/JetBrains/Exposed/pull/1749 +* feat: EXPOSED-47 Add support for SET DEFAULT reference option by @joc-a in https://github.com/JetBrains/Exposed/pull/1744 +* control whether arguments should be inlined or passed in. by @lure in https://github.com/JetBrains/Exposed/pull/1621 +* feat: Add partial index support (Postgres only) by @lure in https://github.com/JetBrains/Exposed/pull/1748 +* add afterStatementPrepared method to StatementInterceptor by @lure in https://github.com/JetBrains/Exposed/pull/1622 +* feat: EXPOSED-60 Support json/json(b) column types by @bog-walk in https://github.com/JetBrains/Exposed/pull/1762 +* feat: EXPOSED-66 Extend partial index to SQLServer and SQLite by @bog-walk in https://github.com/JetBrains/Exposed/pull/1763 +* [EXPOSED-46] Add a possibility to set a delay for the repetition attempts by @mgrati in https://github.com/JetBrains/Exposed/pull/1742 +* feat: EXPOSED-69 Extend json support to H2, Oracle (text) and DAO by @bog-walk in https://github.com/JetBrains/Exposed/pull/1766 +* feat: EXPOSED-68 Add more json/json(b) column functions by @bog-walk in https://github.com/JetBrains/Exposed/pull/1770 +* #623 Add support of window functions in Exposed DSL by @Legohuman in https://github.com/JetBrains/Exposed/pull/1651 +* feat: EXPOSED-89 Support functions in Create Index by @bog-walk in https://github.com/JetBrains/Exposed/pull/1788 +* feat: Add spring mutli container support by @FullOfOrange in https://github.com/JetBrains/Exposed/pull/1781 +* feat: EXPOSED-43 Add support for timestamp with time zone by @joc-a in https://github.com/JetBrains/Exposed/pull/1787 + +Bug fixes: +* Fix an error when updating an entity with a foreign key id (issue 880) by @forketyfork in https://github.com/JetBrains/Exposed/pull/1668 +* EXPOSED-15 Fix running mysql tests on M1 by @e5l in https://github.com/JetBrains/Exposed/pull/1719 +* Fix grammar in error message by @micheljung in https://github.com/JetBrains/Exposed/pull/1717 +* Fix: PostgreSQLDialect.modifyColumn is not able to drop default values by @michael-markl in https://github.com/JetBrains/Exposed/pull/1716 +* Fix UInt value out of bounds by @keta1 in https://github.com/JetBrains/Exposed/pull/1709 +* fix: EXPOSED-16 Failed tests in KotlinTimeTests by @joc-a in https://github.com/JetBrains/Exposed/pull/1724 +* EXPOSED-21 Primary key constraint not created by @bog-walk in https://github.com/JetBrains/Exposed/pull/1728 +* EXPOSED-19 Max timestamp in SQLite not working by @joc-a in https://github.com/JetBrains/Exposed/pull/1725 +* fix: EXPOSED-27 Id is not in record set by @joc-a in https://github.com/JetBrains/Exposed/pull/1731 +* fix: EXPOSED-28 Update with join fails on H2 in MySql mode by @bog-walk in https://github.com/JetBrains/Exposed/pull/1732 +* fix: EXPOSED-29 Cannot set nullable composite column in InsertStatement by @joc-a in https://github.com/JetBrains/Exposed/pull/1733 +* fix: EXPOSED-23 H2 unsupported indexing behavior by @bog-walk in https://github.com/JetBrains/Exposed/pull/1734 +* fix: EXPOSED-31 Landing Readme links and demo code by @bog-walk in https://github.com/JetBrains/Exposed/pull/1736 +* fix: EXPOSED-36 LocalDate comparison in SQLite by @bog-walk in https://github.com/JetBrains/Exposed/pull/1741 +* fix: EXPOSED-42 Can't create BLOB column with default value by @joc-a in https://github.com/JetBrains/Exposed/pull/1740 +* fix: EXPOSED-49 Replace statement defined as upsert statement by @bog-walk in https://github.com/JetBrains/Exposed/pull/1747 +* fix: EXPOSED-48 Incorrect statistics aggregate functions by @bog-walk in https://github.com/JetBrains/Exposed/pull/1745 +* Sum batch results for inserts by @johnzeringue in https://github.com/JetBrains/Exposed/pull/1641 +* fix: EXPOSED-57 BatchInsertStatement can't be used with MySQL upsert by @bog-walk in https://github.com/JetBrains/Exposed/pull/1754 +* fix: EXPOSED-50 customEnumeration reference column error by @bog-walk in https://github.com/JetBrains/Exposed/pull/1785 +* fix: EXPOSED-91 NPE in existingIndices() with function index by @bog-walk in https://github.com/JetBrains/Exposed/pull/1791 +* fix: SQLServerException: The port number -1 is not valid. by @joc-a in https://github.com/JetBrains/Exposed/pull/1789 +* fix: EXPOSED-80 Set repetition policy for suspended transactions by @bog-walk in https://github.com/JetBrains/Exposed/pull/1774 +* fix: Exclude deleted and renamed files from detekt GitHub Action by @joc-a in https://github.com/JetBrains/Exposed/pull/1795 +* fix: EXPOSED-97 Unsigned column types truncate MySQL values by @bog-walk in https://github.com/JetBrains/Exposed/pull/1796 +* fix: EXPOSED-98: Add instructions to log-in to see and log issues by @jasonjmcghee in https://github.com/JetBrains/Exposed/pull/1798 +* fix: EXPOSED-83 createMissingTablesAndColumns not detecting missing PK by @bog-walk in https://github.com/JetBrains/Exposed/pull/1797 +* test: Fix failing exposed-tests in SQL Server by @bog-walk in https://github.com/JetBrains/Exposed/pull/1801 +* fix: EXPOSED-54 CaseWhen.Else returns narrow Expression by @bog-walk in https://github.com/JetBrains/Exposed/pull/1800 +* fix: EXPOSED-99 SchemaUtils incorrectly compares datetime defaults by @bog-walk in https://github.com/JetBrains/Exposed/pull/1802 +* test: Fix failing exposed-tests in Oracle by @bog-walk in https://github.com/JetBrains/Exposed/pull/1803 +* fix: EXPOSED-82 Inaccurate UShort column type mapping by @bog-walk in https://github.com/JetBrains/Exposed/pull/1799 +* test: Fix failing datetime tests in MariaDB by @bog-walk in https://github.com/JetBrains/Exposed/pull/1805 + # 0.41.1 Infrastructure: * Kotlin 1.7.21 diff --git a/docs/ORACLE.md b/docs/ORACLE.md index a8322ef81b..1f73807fc7 100644 --- a/docs/ORACLE.md +++ b/docs/ORACLE.md @@ -7,9 +7,10 @@ * `insert` with `select` with `limit` not supported (`DMLTests.testInsertSelect01`) ### Running tests locally with Gradle -* Run `oracleTest` gradle task + +* Run `testOracle` gradle task ### Running tests locally with Docker -* Run Oracle locally, e.g. with `quillbuilduser/oracle-18-xe` Docker image or use `docker-compose -f docker-compose-oracle.yml up` -* Run tests with `-Dexposed.test.dialects=oracle`, -(optionally you may need to provide `-Dexposed.test.oracle.host=_YOUR_DOCKER_HOST_ -exposed.test.oracle.port=_SQLSERVER_SERVER_EXPOSED_PORT_`) + +* Run Oracle locally with gradle task `oracleComposeUp` +* Run tests with `testOracle` gradle task diff --git a/docs/SQLServer.md b/docs/SQLServer.md index d7e097001d..e4f0e93f3b 100644 --- a/docs/SQLServer.md +++ b/docs/SQLServer.md @@ -1,13 +1,10 @@ -## SQL Server dialect +## SQL Server dialect ### Running tests locally with Gradle -* Run `sqlServerTest` gradle task -### Running tests locally with Docker -* Run SQL Server locally, e.g. with Docker image with command like -`docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d microsoft/mssql-server-linux` -or use `docker-compose -f docker-compose-sqlserver.yml up` +* Run `testSqlserver` gradle task -* Run tests with `-Dexposed.test.dialects=sqlserver`, -(optionally you may need to provide `-Dexposed.test.sqlserver.host=_YOUR_DOCKER_HOST_ -exposed.test.sqlserver.port=_SQLSERVER_SERVER_EXPOSED_PORT_`) +### Running tests locally with Docker +* Run SQL Server locally with gradle task `sqlserverComposeUp` +* Run tests with `testSqlserver` gradle task diff --git a/documentation-website/Writerside/c.list b/documentation-website/Writerside/c.list new file mode 100644 index 0000000000..c4bd745d05 --- /dev/null +++ b/documentation-website/Writerside/c.list @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/documentation-website/Writerside/cfg/buildprofiles.xml b/documentation-website/Writerside/cfg/buildprofiles.xml new file mode 100644 index 0000000000..4e5ceed97b --- /dev/null +++ b/documentation-website/Writerside/cfg/buildprofiles.xml @@ -0,0 +1,7 @@ + + + + true + + diff --git a/documentation-website/Writerside/hi.tree b/documentation-website/Writerside/hi.tree new file mode 100644 index 0000000000..73a79a0d72 --- /dev/null +++ b/documentation-website/Writerside/hi.tree @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation-website/Writerside/images/completion_procedure.png b/documentation-website/Writerside/images/completion_procedure.png new file mode 100644 index 0000000000..3535a3f3fa Binary files /dev/null and b/documentation-website/Writerside/images/completion_procedure.png differ diff --git a/documentation-website/Writerside/images/completion_procedure_dark.png b/documentation-website/Writerside/images/completion_procedure_dark.png new file mode 100644 index 0000000000..a65beb0ea1 Binary files /dev/null and b/documentation-website/Writerside/images/completion_procedure_dark.png differ diff --git a/documentation-website/Writerside/images/convert_table_to_xml.png b/documentation-website/Writerside/images/convert_table_to_xml.png new file mode 100644 index 0000000000..2518a64cd1 Binary files /dev/null and b/documentation-website/Writerside/images/convert_table_to_xml.png differ diff --git a/documentation-website/Writerside/images/convert_table_to_xml_dark.png b/documentation-website/Writerside/images/convert_table_to_xml_dark.png new file mode 100644 index 0000000000..471612264a Binary files /dev/null and b/documentation-website/Writerside/images/convert_table_to_xml_dark.png differ diff --git a/documentation-website/Writerside/images/new_topic_options.png b/documentation-website/Writerside/images/new_topic_options.png new file mode 100644 index 0000000000..ea9744dcf7 Binary files /dev/null and b/documentation-website/Writerside/images/new_topic_options.png differ diff --git a/documentation-website/Writerside/images/new_topic_options_dark.png b/documentation-website/Writerside/images/new_topic_options_dark.png new file mode 100644 index 0000000000..c91963872b Binary files /dev/null and b/documentation-website/Writerside/images/new_topic_options_dark.png differ diff --git a/documentation-website/Writerside/project.ihp b/documentation-website/Writerside/project.ihp new file mode 100644 index 0000000000..00b4482d77 --- /dev/null +++ b/documentation-website/Writerside/project.ihp @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/documentation-website/Writerside/redirection-rules.xml b/documentation-website/Writerside/redirection-rules.xml new file mode 100644 index 0000000000..4cce403462 --- /dev/null +++ b/documentation-website/Writerside/redirection-rules.xml @@ -0,0 +1,44 @@ + + + + + page.html + + + Created after removal of "Introduction" from Exposed Documentation + Introduction.html + + + Created after removal of "Working with Database" from Exposed Documentation + Working-with-databases.html + + + Created after removal of "Working with DataSource" from Exposed Documentation + Working-with-data-sources.html + + + Created after removal of "Working with Transaction" from Exposed Documentation + Working-with-transactions.html + + + " from Exposed Documentation]]> + Samples-md.html + + + Created after removal of "Transactions" from Exposed Documentation + Transactions.html + + + + " from Exposed Documentation]]> + Working-with-Coroutines.html + + + Created after removal of "DAO API" from Exposed Documentation + DAO-API.html + + + Created after removal of "DSL API" from Exposed Documentation + DSL-API.html + + diff --git a/documentation-website/Writerside/topics/Asynchronous-Support.md b/documentation-website/Writerside/topics/Asynchronous-Support.md new file mode 100644 index 0000000000..ed06fe1b31 --- /dev/null +++ b/documentation-website/Writerside/topics/Asynchronous-Support.md @@ -0,0 +1,60 @@ +# Asynchronous Support + +## Working with Coroutines + +In the modern world, non-blocking and asynchronous code is popular. Kotlin +has [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) that give you an imperative way of writing asynchronous code. Most +Kotlin frameworks (like [ktor](https://ktor.io)) have built-in support for Coroutines while Exposed is mostly blocking. Why? Because Exposed uses JDBC API to interact +with databases that was designed in an era of blocking APIs. Moreover, Exposed stores some values in +thread-local variables while coroutines could (and will) be executed in different threads. + +Since Exposed 0.15.1, there are bridge functions that will give you a safe way to interact with Exposed within `suspend` +blocks: `newSuspendedTransaction/Transaction.withSuspendTransaction` have the same parameters as a blocking `transaction` function but will allow you to +provide a `CoroutineDispatcher` in which the function will be executed. If context is not provided, your code will be executed within the current `coroutineContext`. + +Sample usage looks like: + +```kotlin +runBlocking { + transaction { + SchemaUtils.create(FooTable) // Table will be created on a current thread + + newSuspendedTransaction(Dispatchers.Default) { + FooTable.insert { it[id] = 1 } // This insert will be executed in one of Default dispatcher threads + + withSuspendTransaction { + val id = FooTable.select { FooTable.id eq 1 } + .single()()[FooTable.id] // This select also will be executed on some thread from Default dispatcher using the same transaction + } + } + + val result = newSuspendedTransaction(Dispatchers.IO) { + FooTable.select { FooTable.id eq 1 } + .single()[H2Tests.Testing.id] // This select will be executed on some thread from IO dispatcher using the same transaction + } + } +} + +``` + +Please note that such code remains blocking (as it still uses JDBC) and you should not try to share a transaction between multiple threads as it will +lead to undefined behaviour. + +If you want to execute some code asynchronously and use the result later in your code, take a look at `suspendedTransactionAsync` function. + +```kotlin +val launchResult = suspendedTransactionAsync(Dispatchers.IO, db = db) { + FooTable.insert {} + + FooTable.select { FooTable.id eq 1 }.singleOrNull()?.getOrNull(Testing.id) +} + +println("Result: " + (launchResult.await() ?: - 1)) + +``` + +This function will accept the same parameters as `newSuspendedTransaction` above, but returns `Deferred` which you could call `await` on to achieve your +result. + +`suspendedTransactionAsync` is always executed in a new transaction to prevent concurrency issues when queries execution order could be changed +by `CoroutineDispatcher`. diff --git a/documentation-website/Writerside/topics/Data-Types.md b/documentation-website/Writerside/topics/Data-Types.md new file mode 100644 index 0000000000..5eebecdd35 --- /dev/null +++ b/documentation-website/Writerside/topics/Data-Types.md @@ -0,0 +1,176 @@ +# Data Types + +Exposed supports the following data types in the table definition: +* `integer` - translates to DB `INT` +* `short` - translates to DB `SMALLINT` +* `long` - `BIGINT` +* `float` - `FLOAT` +* `decimal` - `DECIMAL` with scale and precision +* `bool` - `BOOLEAN` +* `char` - `CHAR` +* `varchar` - `VARCHAR` with length +* `text` - `TEXT` +* `enumeration` - `INT` ordinal value +* `enumerationByName` - `VARCHAR` +* `customEnumeration` - see [additional section](#how-to-use-database-enum-types) +* `blob` - `BLOB` +* `binary` - `VARBINARY` with length +* `uuid` - `BINARY(16)` +* `reference` - a foreign key + +The `exposed-java-time` extension (`org.jetbrains.exposed:exposed-java-time:$exposed_version`) provides additional types: + +* `date` - `DATETIME` +* `time` - `TIME` +* `datetime` - `DATETIME` +* `timestamp` - `TIMESTAMP` +* `duration` - `DURATION` + + +Some types are different for specific DB dialect. + + +The `exposed-json` extension (`org.jetbrains.exposed:exposed-json:$exposed_version`) provides additional types +(see [how to use](#how-to-use-json-and-jsonb-types)): + +* `json` - `JSON` +* `jsonb` - `JSONB` + + +Databases store JSON values either in text or binary format, so Exposed provides two types to account for any potential +differences, if they exist, for example: + +- **PostgreSQL**: `json()` maps to `JSON`, while `jsonb()` maps to `JSONB`. +- **SQLite**: No native JSON type, so `json()` maps to TEXT, while `jsonb()` throws. +- **MySQL**: JSON type only supports binary format, so `json()` and `jsonb()` both map to JSON. +- **Oracle**: Exposed does not currently support the JSON binary format of Oracle 21c; only text format `json()` can be used. + + +## How to use database ENUM types +Some of the databases (e.g. MySQL, PostgreSQL, H2) support explicit ENUM types. Because keeping such columns in sync with +Kotlin enumerations using only JDBC metadata could be a huge challenge, Exposed doesn't provide a possibility to manage +such columns in an automatic way, but that doesn't mean that you can't use such column types. +You have two options to work with ENUM database types: +1. Use existing ENUM column from your tables +2. Create column from Exposed by providing raw definition SQL + In both cases, you should use `customEnumeration` function (available since version 0.10.3) + +As a jdbc-driver can provide/expect specific classes for Enum type, you must provide from/to transformation functions for +them when defining a `customEnumeration`. + +For such enum `private enum class Foo { Bar, Baz }`, you can use the provided code for your database: + +**H2** +```Kotlin +val existingEnumColumn = customEnumeration("enumColumn", { Foo.values()[it as Int] }, { it.name }) +val newEnumColumn = customEnumeration("enumColumn", "ENUM('Bar', 'Baz')", { Foo.values()[it as Int] }, { it.name }) +``` + +**MySQL** +```Kotlin +val existingEnumColumn = customEnumeration("enumColumn", { value -> Foo.valueOf(value as String) }, { it.name }) +val newEnumColumn = customEnumeration("enumColumn", "ENUM('Bar', 'Baz')", { value -> Foo.valueOf(value as String) }, { it.name }) +``` + +**PostgreSQL** + +PostgreSQL requires that ENUM is defined as a separate type, so you have to create it before creating your table. +Also, PostgreSQL JDBC driver returns PGobject instances for such values. The full working sample is provided below: +```Kotlin +class PGEnum>(enumTypeName: String, enumValue: T?) : PGobject() { + init { + value = enumValue?.name + type = enumTypeName + } +} + +object EnumTable : Table() { + val enumColumn = customEnumeration("enumColumn", "FooEnum", {value -> Foo.valueOf(value as String)}, { PGEnum("FooEnum", it) } +} +... +transaction { + exec("CREATE TYPE FooEnum AS ENUM ('Bar', 'Baz');") + SchemaUtils.create(EnumTable) + ... +} +``` + +## How to use Json and JsonB types + +Add the following dependencies to your `build.gradle.kts`: +```kotlin +val exposedVersion: String by project + +dependencies { + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-json:$exposedVersion") +} +``` + +Exposed works together with [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) to support +`@Serializable` classes and JSON serialization/deserialization: +```kotlin +@Serializable +data class Project(val name: String, val language: String, val active: Boolean) + +val format = Json { prettyPrint = true } + +object Team : Table("team") { + val groupId = varchar("group_id", 32) + val project = json("project", format) // equivalent to json("project", format, Project.serializer()) +} + +transaction { + val mainProject = Project("Main", "Java", true) + Team.insert { + it[groupId] = "A" + it[project] = mainProject + } + Team.update({ Team.groupId eq "A" }) { + it[project] = mainProject.copy(language = "Kotlin") + } + + Team.selectAll().map { "Team ${it[Team.groupId]} -> ${it[Team.project]}" }.forEach { println(it) } + // Team A -> Project(name=Main, language=Kotlin, active=true) +} +``` + +Both column types also support custom serializer and deserializer arguments, using the form: +```kotlin +fun json(name: String, serialize: (T) -> String, deserialize: (String) -> T): Column +``` + +### Json Functions + +JSON path strings can be used to extract values (either as JSON or as a scalar value) at a specific field/key: +```kotlin +val projectName = Team.project.extract("name") +val languageIsKotlin = Team.project.extract("language").lowerCase() eq "kotlin" +Team.slice(projectName).select { languageIsKotlin }.map { it[projectName] } +``` + + +Databases that support a path context root `$` will have this value appended to the generated SQL path expression +by default, so it is not necessary to include it in the provided argument String. In the above example, if MySQL is being +used, the provided path arguments should be `.name` and `.language` respectively. + + +The JSON functions `exists()` and `contains()` are currently supported as well: +```kotlin +val hasActiveStatus = Team.project.exists(".active") +val activeProjects = Team.select { hasActiveStatus }.count() + +// Depending on the database, filter paths can be provided instead, as well as optional arguments +// PostgreSQL example +val mainId = "Main" +val hasMainProject = Team.project.exists(".name ? (@ == \$main)", optional = "{\"main\":\"$mainId\"}") +val mainProjects = Team.select { hasMainProject }.map { it[Team.groupId] } + +val usesKotlin = Team.project.contains("{\"language\":\"Kotlin\"}") +val kotlinTeams = Team.select { usesKotlin }.count() + +// Depending on the database, an optional path can be provided too +// MySQL example +val usesKotlin = Team.project.contains("\"Kotlin\"", ".language") +val kotlinTeams = Team.select { usesKotlin }.count() +``` diff --git a/documentation-website/Writerside/topics/Databases.md b/documentation-website/Writerside/topics/Databases.md new file mode 100644 index 0000000000..c1b8d356f4 --- /dev/null +++ b/documentation-website/Writerside/topics/Databases.md @@ -0,0 +1,7 @@ +# Databases + +[Working with Database](Working-with-Database.md) + +[Working with DataSource](Working-with-DataSource.md) + +[Working with Transaction](Working-with-Transaction.md) diff --git a/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md b/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md new file mode 100644 index 0000000000..ae35dd3a9d --- /dev/null +++ b/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md @@ -0,0 +1,351 @@ +# Deep Dive into DAO + +## Overview +The DAO (Data Access Object) API of Exposed, is similar to ORM frameworks like Hibernate with a Kotlin-specific API. +A DB table is represented by an `object` inherited from `org.jetbrains.exposed.sql.Table` like this: +```kotlin +object StarWarsFilms : Table() { + val id: Column = integer("id").autoIncrement() + val sequelId: Column = integer("sequel_id").uniqueIndex() + val name: Column = varchar("name", 50) + val director: Column = varchar("director", 50) + override val primaryKey = PrimaryKey(id, name = "PK_StarWarsFilms_Id") // PK_StarWarsFilms_Id is optional here +} +``` +Tables that contain an `Int` id with the name `id` can be declared like this: +```kotlin +object StarWarsFilms : IntIdTable() { + val sequelId: Column = integer("sequel_id").uniqueIndex() + val name: Column = varchar("name", 50) + val director: Column = varchar("director", 50) +} +``` +Note that these Column types will be defined automatically, so you can also just leave them out. This would produce the same result as the example above: +```kotlin +object StarWarsFilms : IntIdTable() { + val sequelId = integer("sequel_id").uniqueIndex() + val name = varchar("name", 50) + val director = varchar("director", 50) +} +``` +An entity instance or a row in the table is defined as a class instance: + ```kotlin +class StarWarsFilm(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(StarWarsFilms) + var sequelId by StarWarsFilms.sequelId + var name by StarWarsFilms.name + var director by StarWarsFilms.director +} +``` +## Basic CRUD operations +### Create +```kotlin +val movie = StarWarsFilm.new { + name = "The Last Jedi" + sequelId = 8 + director = "Rian Johnson" +} +``` +### Read +To get entities use one of the following +```kotlin +val movies = StarWarsFilm.all() +val movies = StarWarsFilm.find { StarWarsFilms.sequelId eq 8 } +val movie = StarWarsFilm.findById(5) +``` +* For a list of available predicates see [DSL Where expression](https://github.com/JetBrains/Exposed/wiki/DSL#where-expression). + Read a value from a property similar to any property in a Kotlin class: +```kotlin +val name = movie.name +``` +#### Sort (Order-by) +Ascending order: +```kotlin +val movies = StarWarsFilm.all().sortedBy { it.sequelId } +``` +Descending order: +```kotlin +val movies = StarWarsFilm.all().sortedByDescending{ it.sequelId } +``` +### Update +Update a value of a property similar to any property in a Kotlin class: +```kotlin +movie.name = "Episode VIII – The Last Jedi" +``` +* Note: Exposed doesn't make an immediate update when you set a new value for Entity, it just stores it on the inner map. "Flushing" values to the database occurs at the end of the transaction or before next `select *` from the database. +### Delete +```kotlin +movie.delete() +``` +## Referencing +### many-to-one reference +Let's say you have this table: +```kotlin +object Users : IntIdTable() { + val name = varchar("name", 50) +} +class User(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Users) + var name by Users.name +} +``` +And now you want to add a table referencing this table (and other tables!): +```kotlin +object UserRatings : IntIdTable() { + val value = long("value") + val film = reference("film", StarWarsFilms) + val user = reference("user", Users) +} +class UserRating(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(UserRatings) + var value by UserRatings.value + var film by StarWarsFilm referencedOn UserRatings.film // use referencedOn for normal references + var user by User referencedOn UserRatings.user +} +``` +Now you can get the film for a `UserRating` object, `filmRating`, in the same way you would get any other field: +```kotlin +filmRating.film // returns a StarWarsFilm object +``` +Now if you wanted to get all the ratings for a film, you could do that by using the `filmRating.find` function, but it is much easier to just add a `referrersOn` field to the `StarWarsFilm` class: +```kotlin +class StarWarsFilm(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(StarWarsFilms) + ... + val ratings by UserRating referrersOn UserRatings.film // make sure to use val and referrersOn + ... +} +``` +You can then access this field on a `StarWarsFilm` object, `movie`: +```kotlin +movie.ratings // returns all UserRating objects with this movie as film +``` +Now imagine a scenario where a user only ever rates a single film. If you want to get the single rating for that user, you can add a `backReferencedOn` field to the `User` class to access the `UserRating` table data: +```kotlin +class User(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Users) + ... + val rating by UserRating backReferencedOn UserRatings.user // make sure to use val and backReferencedOn +} +``` +You can then access this field on a `User` object, `user1`: +```kotlin +user1.rating // returns a UserRating object +``` +### Optional reference +You can also add an optional reference: +```kotlin +object UserRatings: IntIdTable() { + ... + val secondUser = reference("second_user", Users).nullable() // this reference is nullable! + ... +} +class UserRating(id: EntityID): IntEntity(id) { + companion object : IntEntityClass(UserRatings) + ... + var secondUser by User optionalReferencedOn UserRatings.secondUser // use optionalReferencedOn for nullable references + ... +} +``` +Now `secondUser` will be a nullable field, and `optionalReferrersOn` should be used instead of `referrersOn` to get all the ratings for a `secondUser`. + +```kotlin +class User(id: EntityID): IntEntity(id) { + companion object : IntEntityClass(Users) + ... + val secondRatings by UserRating optionalReferrersOn UserRatings.secondUser // make sure to use val and optionalReferrersOn + ... +} +``` + +### many-to-many reference +In some cases, a many-to-many reference may be required. +Let's assume you want to add a reference to the following Actors table to the StarWarsFilm class: +```kotlin +object Actors: IntIdTable() { + val firstname = varchar("firstname", 50) + val lastname = varchar("lastname", 50) +} +class Actor(id: EntityID): IntEntity(id) { + companion object : IntEntityClass(Actors) + var firstname by Actors.firstname + var lastname by Actors.lastname +} +``` +Create an additional intermediate table to store the references: +```kotlin +object StarWarsFilmActors : Table() { + val starWarsFilm = reference("starWarsFilm", StarWarsFilms) + val actor = reference("actor", Actors) + override val primaryKey = PrimaryKey(starWarsFilm, actor, name = "PK_StarWarsFilmActors_swf_act") // PK_StarWarsFilmActors_swf_act is optional here +} +``` +Add a reference to `StarWarsFilm`: +```kotlin +class StarWarsFilm(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(StarWarsFilms) + ... + var actors by Actor via StarWarsFilmActors + ... +} +``` +Note: You can set up IDs manually inside a transaction like this: +```kotlin +transaction { + //only works with UUIDTable and UUIDEntity + StarWarsFilm.new (UUID.randomUUID()){ + ... + actors = SizedCollection(listOf(actor)) + } +} +``` +### Parent-Child reference +Parent-child reference is very similar to many-to-many version, but an intermediate table contains both references to the same table. +Let's assume you want to build a hierarchical entity which could have parents and children. Our tables and an entity mapping will look like +```kotlin +object NodeTable : IntIdTable() { + val name = varchar("name", 50) +} +object NodeToNodes : Table() { + val parent = reference("parent_node_id", NodeTable) + val child = reference("child_user_id", NodeTable) +} +class Node(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(NodeTable) + var name by NodeTable.name + var parents by Node.via(NodeToNodes.child, NodeToNodes.parent) + var children by Node.via(NodeToNodes.parent, NodeToNodes.child) +} +``` +As you can see `NodeToNodes` columns target only `NodeTable` and another version of `via` function were used. +Now you can create a hierarchy of nodes. +```kotlin +val root = Node.new { name = "root" } +val child1 = Node.new { + name = "child1" +} +child1.parents = SizedCollection(root) // assign parent +val child2 = Node.new { name = "child2" } +root.children = SizedCollection(listOf(child1, child2)) // assign children +``` + +### Eager Loading +**Available since 0.13.1**. +References in Exposed are lazily loaded, meaning queries to fetch the data for the reference are made at the moment the reference is first utilised. For scenarios wherefore you know you will require references ahead of time, Exposed can eager load them at the time of the parent query, this is prevents the classic "N+1" problem as references can be aggregated and loaded in a single query. +To eager load a reference you can call the "load" function and pass the DAO's reference as a KProperty: +```kotlin +StarWarsFilm.findById(1).load(StarWarsFilm::actors) +``` +This works for references of references also, for example if Actors had a rating reference you could: +```kotlin +StarWarsFilm.findById(1).load(StarWarsFilm::actors, Actor::rating) +``` +Similarly you can eager load references on Collections of DAO's such as Lists and SizedIterables, for collections you can use the with function in the same fashion as before, passing the DAO's references as KProperty's. +```kotlin +StarWarsFilm.all().with(StarWarsFilm::actors) +``` +NOTE: References that are eagerly loaded are stored inside the transaction cache; this means that they are not available in other transactions and thus must be loaded and referenced inside the same transaction. As of [0.35.1](https://github.com/JetBrains/Exposed/blob/master/docs/ChangeLog.md#0351:~:text=References%20can%20be%20stored%20within%20an%20Entity%20with%20enabled%20keepLoadedReferencesOutOfTransaction%20config%20parameter.%20It%20will%20allow%20getting%20referenced%20values%20outside%20the%20transaction%20block.), however, enabling `keepLoadedReferencesOutOfTransaction` in `DatabaseConfig` will allow getting referenced values outside the transaction block. + +#### Eager loading for Text Fields +Some database drivers do not load text content immediately (for performance and memory reasons) which means that you can obtain the column value only within the open transaction. + +If you desire to make content available outside the transaction, you can use the eagerLoading param when defining the DB Table. +```kotlin +object StarWarsFilms : Table() { + ... + val description = text("name", eagerLoading=true) +} +``` +## Advanced CRUD operations +### Read entity with a join to another table +Let's imagine that you want to find all users who rated second SW film with more than 5. +First of all, we should write that query using Exposed DSL. +```kotlin +val query = Users.innerJoin(UserRatings).innerJoin(StarWarsFilm) + .slice(Users.columns) + .select { + StarWarsFilms.sequelId eq 2 and (UserRatings.value gt 5) + }.withDistinct() +``` +After that all we have to do is to "wrap" a result with User entity: +```kotlin +val users = User.wrapRows(query).toList() +``` +### Auto-fill created and updated columns on entity change +See example by @PaulMuriithi [here](https://github.com/PaulMuriithi/ExposedDatesAutoFill/blob/master/src/main/kotlin/app/Models.kt). +### Use queries as expressions +Imagine that you want to sort cities by how many users each city has. In order to do so, you can write a sub-query which counts users in each city and order by that number. Though in order to do so you'll have to convert `Query` to `Expression`. This can be done using `wrapAsExpression` function: +```kotlin +val expression = wrapAsExpression(Users + .slice(Users.id.count()) + .select { + Cities.id eq Users.cityId + }) +val cities = Cities + .selectAll() + .orderBy(expression, SortOrder.DESC) + .toList() +``` + +### Add computed fields to entity class +Imagine that you want to use a window function to rank films with each entity fetch. The companion object of the entity class can override any open function in `EntityClass`, but to achieve this functionality only `searchQuery()` needs to +be overriden. The results of the function can then be accessed using a property of the entity class: +```kotlin +object StarWarsFilms : IntIdTable() { + val sequelId = integer("sequel_id").uniqueIndex() + val name = varchar("name", 50) + val rating = double("rating") + + val rank = Rank().over().orderBy(rating, SortOrder.DESC) +} + +class StarWarsFilm(id: EntityID) : IntEntity(id) { + var sequelId by StarWarsFilms.sequelId + var name by StarWarsFilms.name + var rating by StarWarsFilms.rating + + val rank: Long + get() = readValues[StarWarsFilms.rank] + + companion object : IntEntityClass(StarWarsFilms) { + override fun searchQuery(op: Op): Query { + return super.searchQuery(op).adjustSlice { + slice(columns + StarWarsFilms.rank) + } + } + } +} + +transaction { + StarWarsFilm.new { + sequelId = 8 + name = "The Last Jedi" + rating = 4.2 + } + // more insertions ... + entityCache.clear() + + // fetch entities with value (or store entities then read value) + StarWarsFilm.find { StarWarsFilms.name like "The%" }.map { it.name to it.rank } +} +``` + +## Entities mapping +### Fields transformation +As databases could store only basic types like integers and strings it's not always conveniently to keep the same simplicity on DAO level. +Sometimes you may want to make some transformations like parsing json from a varchar column or get some value from a cache based on value from a database. +In that case the preferred way is to use column transformations. Assume that we want to define unsigned integer field on Entity, but Exposed doesn't have such column type yet. +```kotlin +object TableWithUnsignedInteger : IntIdTable() { + val uint = integer("uint") +} +class EntityWithUInt : IntEntity() { + var uint: UInt by TableWithUnsignedInteger.uint.transform({ it.toInt() }, { it.toUInt() }) + + companion object : IntEntityClass() +} +``` +`transform` function accept two lambdas to convert values to and from an original column type. +After that in your code you'll be able to put only `UInt` instances into `uint` field. +It still possible to insert/update values with negative integers via DAO, but your business code becomes much cleaner. +Please keep in mind what such transformations will aqure on every access to a field what means that you should avoid heavy transformations here. diff --git a/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md b/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md new file mode 100644 index 0000000000..73eb3beeed --- /dev/null +++ b/documentation-website/Writerside/topics/Deep-Dive-into-DSL.md @@ -0,0 +1,583 @@ +# Deep Dive into DSL + +## Overview + +The DSL (Domain-Specific Language) API of Exposed is similar to actual SQL statements, but with the type safety that Kotlin offers. + +A database table is represented by an `object` inherited from `org.jetbrains.exposed.sql.Table` like this: + +```kotlin +object StarWarsFilms : Table() { + val id: Column = integer("id").autoIncrement() + val sequelId: Column = integer("sequel_id").uniqueIndex() + val name: Column = varchar("name", 50) + val director: Column = varchar("director", 50) + override val primaryKey = PrimaryKey(id, name = "PK_StarWarsFilms_Id") // PK_StarWarsFilms_Id is optional here +} +``` + +Tables that contains `Int` id with the name `id` can be declared like this: + +```kotlin +object StarWarsFilms : IntIdTable() { + val sequelId: Column = integer("sequel_id").uniqueIndex() + val name: Column = varchar("name", 50) + val director: Column = varchar("director", 50) +} +``` + +## CRUD operations + +CRUD stands for Create Read Update Delete, which are four basic operations for a database to support. This section shows how to perform SQL CRUD operations +using Kotlin DSL. + +### Create + +To create a new table row, you use the `insert` query. Exposed provides several functions to insert rows into a table: + +* `insert` adds a new row. If the same row already exists in the table, it throws an exception. + ```kotlin + // SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) + // VALUES ('RUSSIA', 'St. Petersburg', 300) + Cities.insert { + it[name] = "St. Petersburg" + it[country] = Country.RUSSIA + it[population] = 500 + } + ``` +* `insertAndGetId` adds a new row and returns its ID. If the same row already exists in the table, it throws an exception. Works only with IntIdTable() tables. + ```kotlin + // SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) + // VALUES ('RUSSIA', 'St. Petersburg', 300) + val id = Cities.insertAndGetId { + it[name] = "St. Petersburg" + it[country] = Country.RUSSIA + it[population] = 500 + } + ``` +* `insertIgnore` adds a new row. If the same row already exists in the table, it ignores it and doesn't throw an exception. This function is supported only for MySQL. + ```kotlin + // SQL: INSERT IGNORE INTO CITIES (COUNTRY, "NAME", POPULATION) + // VALUES ('RUSSIA', 'St. Petersburg', 300) + val id = Cities.insertIgnore { + it[name] = "St. Petersburg" + it[country] = Country.RUSSIA + it[population] = 500 + } + ``` +* `insertIgnoreAndGetId` adds a new row and returns its ID. If the same row already exists in the table, it ignores it and doesn't throw an exception. This function + is supported only for MySQL. Works only with IntIdTable() tables. + ```kotlin + // SQL: INSERT IGNORE INTO CITIES (COUNTRY, "NAME", POPULATION) + // VALUES ('RUSSIA', 'St. Petersburg', 300) + val id = Cities.insertIgnoreAndGetId { + it[name] = "St. Petersburg" + it[country] = Country.RUSSIA + it[population] = 500 + } + ``` + +```kotlin +val id = StarWarsFilms.insertAndGetId { + it[name] = "The Last Jedi" + it[sequelId] = 8 + it[director] = "Rian Johnson" +} +``` + +### Read + +```kotlin +val query: Query = StarWarsFilms.select { StarWarsFilms.sequelId eq 8 } +``` + +`Query` inherit `Iterable` so it is possible to traverse it with map/foreach etc'. For example: + +```kotlin +StarWarsFilms.select { StarWarsFilms.sequelId eq 8 }.forEach { + println(it[StarWarsFilms.name]) +} +``` + +There is `slice` function which allows you to select specific columns or/and expressions. + +```kotlin +val filmAndDirector = StarWarsFilms.slice(StarWarsFilms.name, StarWarsFilms.director).selectAll().map { + it[StarWarsFilms.name] to it[StarWarsFilms.director] +} +``` + +If you want to select only distinct value then use `withDistinct()` function: + +```kotlin +val directors = StarWarsFilms.slice(StarWarsFilms.director).select { StarWarsFilms.sequelId less 5 }.withDistinct().map { + it[StarWarsFilms.director] +} +``` + +### Update + +```kotlin +StarWarsFilms.update({ StarWarsFilms.sequelId eq 8 }) { + it[StarWarsFilms.name] = "Episode VIII – The Last Jedi" +} +``` + +If you want to update column value with some expression like increment use `update` function or setter: + +```kotlin +StarWarsFilms.update({ StarWarsFilms.sequelId eq 8 }) { + with(SqlExpressionBuilder) { + it.update(StarWarsFilms.sequelId, StarWarsFilms.sequelId + 1) + // or + it[StarWarsFilms.sequelId] = StarWarsFilms.sequelId + 1 + } +} +``` + +### Delete + +```kotlin +StarWarsFilms.deleteWhere { StarWarsFilms.sequelId eq 8 } +``` + +## Where expression + +Query expression (where) expects a boolean operator (ie: `Op`). +Allowed conditions are: + +``` +eq - (==) +neq - (!=) +isNull() +isNotNull() +less - (<) +lessEq - (<=) +greater - (>) +greaterEq - (>=) +like - (=~) +notLike - (!~) +exists +notExists +regexp +notRegexp +inList +notInList +between +match (MySQL MATCH AGAINST) +isDistinctFrom (null-safe equality comparison) +isNotDistinctFrom (null-safe equality comparison) +``` + +Allowed logical conditions are: + +``` +not +and +or +andIfNotNull +orIfNotNull +compoundAnd() +compoundOr() +``` + +## Conditional where + +It is a rather common case to have a query with a `where` clause that depends on some other code's conditions. Moreover, independent or nested conditions could +make it more complicated to prepare such `where` clauses. +Let's imagine that we have a form on a website where a user can optionally filter "Star Wars" films by a director and/or a sequel. +In Exposed version before 0.8.1 you had to code it like: + +```Kotlin +val condition = when { + directorName!=null && sequelId!=null -> + Op.build { StarWarsFilms.director eq directorName and (StarWarsFilms.sequelId eq sequelId) } + directorName!=null -> + Op.build { StarWarsFilms.director eq directorName } + sequelId!=null -> + Op.build { StarWarsFilms.sequelId eq sequelId } + else -> null +} +val query = condition?.let { StarWarsFilms.select(condition) } ?: StarWarsFilms.selectAll() +``` + +or + +```Kotlin +val query = when { + directorName!=null && sequelId!=null -> + StarWarsFilms.select { StarWarsFilms.director eq directorName and (StarWarsFilms.sequelId eq sequelId) } + directorName!=null -> + StarWarsFilms.select { StarWarsFilms.director eq directorName } + sequelId!=null -> + StarWarsFilms.select { StarWarsFilms.sequelId eq sequelId } + else -> StarWarsFilms.selectAll() +} +``` + +This is a very primitive example, but you should get the main idea about the problem. +Now let's try to write the same query in a more simple way (`andWhere` function available since 0.10.5): + +```Kotlin +val query = StarWarsFilms.selectAll() +directorName?.let { + query.andWhere { StarWarsFilms.director eq it } +} +sequelId?.let { + query.andWhere { StarWarsFilms.sequelId eq it } +} +``` + +But what if we want to conditionally select from another table and join it only when a condition is true? +You have to use `adjustColumnSet` and `adjustSlice` functions (available since 0.8.1) which allows to extend and modify `join` and `slice` parts of a query (see kdoc +on that functions): + +```Kotlin +actorName?.let { + query.adjustColumnSet { innerJoin(Actors, { StarWarsFilms.sequelId }, { Actors.sequelId }) } + .adjustSlice { slice(fields + Actors.columns) } + .andWhere { Actors.name eq actorName } +} +``` + +## Count + +`count()` is a method of `Query` that is used like below example: + +```kotlin +val count = StarWarsFilms.select { StarWarsFilms.sequelId eq 8 }.count() +``` + +## Order-by + +Order-by accepts a list of columns mapped to boolean indicates if sorting should be ascending or descending. +Example: + +```kotlin +StarWarsFilms.selectAll().orderBy(StarWarsFilms.sequelId to SortOrder.ASC) +``` + +## Group-by + +In group-by, define fields and their functions (such as `count`) by the `slice()` method. + +```kotlin +StarWarsFilms + .slice(StarWarsFilms.sequelId.count(), StarWarsFilms.director) + .selectAll() + .groupBy(StarWarsFilms.director) +``` + +Available functions are: + +``` +count +sum +max +min +average +... +``` + +## Limit + +You can use limit function to prevent loading large data sets or use it for pagination with second `offset` parameter. + +```kotlin +// Take 2 films after the first one. +StarWarsFilms.select { StarWarsFilms.sequelId eq Actors.sequelId }.limit(2, offset = 1) +``` + +## Join + +For the join examples below, consider the following tables: + +```kotlin +object StarWarsFilms : IntIdTable() { + val sequelId: Column = integer("sequel_id").uniqueIndex() + val name: Column = varchar("name", 50) + val director: Column = varchar("director", 50) +} +object Actors : IntIdTable() { + val sequelId: Column = integer("sequel_id").uniqueIndex() + val name: Column = varchar("name", 50) +} +object Roles : Table() { + val sequelId: Column = integer("sequel_id") + val actorId: Column> = reference("actor_id", Actors) + val characterName: Column = varchar("name", 50) +} +``` + +Join to count how many actors star in each movie: + +```kotlin +Actors.join(StarWarsFilms, JoinType.INNER, onColumn = Actors.sequelId, otherColumn = StarWarsFilms.sequelId) + .slice(Actors.name.count(), StarWarsFilms.name) + .selectAll() + .groupBy(StarWarsFilms.name) +``` + +Instead of specifying `onColumn` and `otherColumn`, `additionalConstraint` can be used (and allows specifying +other types of join conditions). + +```kotlin +Actors.join(StarWarsFilms, JoinType.INNER, additionalConstraint = { StarWarsFilms.sequelId eq Actors.sequelId }) + .slice(Actors.name.count(), StarWarsFilms.name) + .selectAll() + .groupBy(StarWarsFilms.name) +``` + +When joining on a foreign key, the more concise `innerJoin` can be used: + +```kotlin +(Actors innerJoin Roles) + .slice(Roles.characterName.count(), Actors.name) + .selectAll() + .groupBy(Actors.name) + .toList() +``` + +This is equivalent to the following: + +```kotlin +Actors.join(Roles, JoinType.INNER, onColumn = Actors.id, otherColumn = Roles.actorId) + .slice(Roles.characterName.count(), Actors.name) + .selectAll() + .groupBy(Actors.name) + .toList() +``` + +## Union + +You can combine the results of multiple queries using using `.union(...)`. +Per the SQL specification, the queries must have the same number of columns, and not be marked for update. +Subqueries may be combined when supported by the database. + +```kotlin +val lucasDirectedQuery = StarWarsFilms.slice(StarWarsFilms.name).select { StarWarsFilms.director eq "George Lucas" } +val abramsDirectedQuery = StarWarsFilms.slice(StarWarsFilms.name).select { StarWarsFilms.director eq "J.J. Abrams" } +val filmNames = lucasDirectedQuery.union(abramsDirectedQuery).map { it[StarWarsFilms.name] } +``` + +Only unique rows are returned by default. Duplicates may be returned using `.unionAll()`. + +```kotlin +val lucasDirectedQuery = StarWarsFilms.slice(StarWarsFilms.name).select { StarWarsFilms.director eq "George Lucas" } +val originalTrilogyQuery = StarWarsFilms.slice(StarWarsFilms.name).select { StarWarsFilms.sequelId inList (3..5) } +val filmNames = lucasDirectedQuery.unionAll(originalTrilogyQuery).map { it[StarWarsFilms.name] } +``` + +## Alias + +Aliases allow preventing ambiguity between field names and table names. +Use the aliased var instead of original one: + +```Kotlin +val filmTable1 = StarWarsFilms.alias("ft1") +filmTable1.selectAll() // can be used in joins etc' +``` + +Also, aliases allow you to use the same table in a join multiple times: + +```Kotlin +val sequelTable = StarWarsFilms.alias("sql") +val originalAndSequelNames = StarWarsFilms + .innerJoin(sequelTable, { StarWarsFilms.sequelId }, { sequelTable[StarWarsFilms.id] }) + .slice(StarWarsFilms.name, sequelTable[StarWarsFilms.name]) + .selectAll() + .map { it[StarWarsFilms.name] to it[sequelTable[StarWarsFilms.name]] } +``` + +And they can be used when selecting from sub-queries: + +```kotlin +val starWarsFilms = StarWarsFilms + .slice(StarWarsFilms.id, StarWarsFilms.name) + .selectAll() + .alias("swf") +val id = starWarsFilms[StarWarsFilms.id] +val name = starWarsFilms[StarWarsFilms.name] +starWarsFilms + .slice(id, name) + .selectAll() + .map { it[id] to it[name] } +``` + +## Schema + +You can create a schema or drop an existing one: + +```Kotlin +val schema = Schema("my_schema") // my_schema is the schema name. +// Creates a Schema +SchemaUtils.createSchema(schema) +// Drops a Schema +SchemaUtils.dropSchema(schema) +``` + +Also, you can specify the schema owner like this (some databases require the explicit owner) : + +```Kotlin +val schema = Schema("my_schema", authorization = "owner") +``` + +If you have many schemas and you want to set a default one, you can use: + +```Kotlin +SchemaUtils.setSchema(schema) +``` + +## Sequence + +If you want to use Sequence, Exposed allows you to: + +### Define a Sequence + +```Kotlin +val myseq = Sequence("my_sequence") // my_sequence is the sequence name. +``` + +Several parameters can be specified to control the properties of the sequence: + +```Kotlin +private val myseq = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 10, + cycle = true, + cache = 20 +) +``` + +### Create and Drop a Sequence + +```Kotlin +// Creates a sequence +SchemaUtils.createSequence(myseq) +// Drops a sequence +SchemaUtils.dropSequence(myseq) +``` + +### Use the NextVal function + +You can use the nextVal function like this: + +```Kotlin +val nextVal = myseq.nextVal() +val id = StarWarsFilms.insertAndGetId { + it[id] = nextVal + it[name] = "The Last Jedi" + it[sequelId] = 8 + it[director] = "Rian Johnson" +} +``` + +```Kotlin +val firstValue = StarWarsFilms.slice(nextVal).selectAll().single()[nextVal] +``` + +## Batch Insert + +Batch Insert allow mapping a list of entities into DB raws in one sql statement. It is more efficient than inserting one by one as it initiates only one statement. +Here is an example: + +```kotlin +val cityNames = listOf("Paris", "Moscow", "Helsinki") +val allCitiesID = cities.batchInsert(cityNames) { name -> + this[cities.name] = name +} +``` + +*NOTE:* The `batchInsert` function will still create multiple `INSERT` statements when interacting with your database. You most likely want to couple this with +the `rewriteBatchedInserts=true` (or `rewriteBatchedStatements=true`) option of your relevant JDBC driver, which will convert those into a single bulkInsert. +You can find the documentation for this option for MySQL [here](https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html) and +PostgreSQL [here](https://jdbc.postgresql.org/documentation/use/). + +If you don't need to get the newly generated values (example: auto incremented ID), set the `shouldReturnGeneratedValues` parameter to false, this increases the +performance of batch inserts by batching them in chunks, instead of always waiting for the database to synchronize the newly inserted object state. + +If you want to check if the `rewriteBatchedInserts` + `batchInsert` is working correctly, check how to enable JDBC logging for your driver because Exposed will always +show the non-rewritten multiple inserts. You can find the documentation for how to enable logging in +PostgreSQL [here](https://jdbc.postgresql.org/documentation/logging/). + +## Insert From Select + +If you want to use `INSERT INTO ... SELECT ` SQL clause try Exposed analog `Table.insert(Query)`. + +```kotlin +val substring = users.name.substring(1, 2) +cities.insert(users.slice(substring).selectAll().orderBy(users.id).limit(2)) +``` + +By default it will try to insert into all non auto-increment `Table` columns in order they defined in Table instance. If you want to specify columns or change the +order, provide list of columns as second parameter: + +```kotlin +val userCount = users.selectAll().count() +users.insert(users.slice(stringParam("Foo"), Random().castTo(VarCharColumnType()).substring(1, 10)).selectAll(), columns = listOf(users.name, users.id)) +``` + +## Insert Or Ignore + +If supported by your specific database, `insertIgnore()` allows insert statements to be executed without throwing any +ignorable errors. This may be useful, for example, when insertion conflicts are possible: +```kotlin +StarWarsFilms.insert { + it[sequelId] = 8 // column pre-defined with a unique index + it[name] = "The Last Jedi" + it[director] = "Rian Johnson" +} +// If insert() was used, this would throw a constraint violation exception +// Instead, this new row is ignored and discarded +StarWarsFilms.insertIgnore { + it[sequelId] = 8 + it[name] = "The Rise of Skywalker" + it[director] = "JJ Abrams" +} +``` + +## Insert Or Update + +Insert or update (Upsert) is a database operation that either inserts a new row or updates an existing row if a duplicate +constraint already exists. The supported functionality of `upsert()` is dependent on the specific database being used. +For example, MySQL's `INSERT ... ON DUPLICATE KEY UPDATE` statement automatically assesses the primary key and unique indices +for a duplicate value, so using the function in Exposed would look like this: +```kotlin +// inserts a new row +StarWarsFilms.upsert { + it[sequelId] = 9 // column pre-defined with a unique index + it[name] = "The Rise of Skywalker" + it[director] = "Rian Johnson" +} +// updates existing row with the correct [director] +StarWarsFilms.upsert { + it[sequelId] = 9 + it[name] = "The Rise of Skywalker" + it[director] = "JJ Abrams" +} +``` +Using another example, PostgreSQL allows more control over which key constraint columns to check for conflict, whether different +values should be used for an update, and whether the update statement should have a `WHERE` clause: +```kotlin +val incrementSequelId = listOf(StarWarsFilms.sequelId to StarWarsFilms.sequelId.plus(1)) +StarWarsFilms.upsert( + StarWarsFilms.sequelId, + onUpdate = incrementSequelId, + where = { StarWarsFilms.director like stringLiteral("JJ%") } +) { + it[sequelId] = 9 + it[name] = "The Rise of Skywalker" + it[director] = "JJ Abrams" +} +``` +If a specific database supports user-defined key columns and none are provided, the table's primary key is used. If there +is no defined primary key, the first unique index is used. If there are no unique indices, each database handles this case +differently, so it is strongly advised that keys are defined to avoid unexpected results. + + +Databases that do not support a specific upsert command implement the standard `MERGE USING` statement with aliases +and a derived table. These include Oracle, SQL Server, and H2 compatibility modes (except for MySQL mode). + diff --git a/documentation-website/Writerside/topics/Default-topic.md b/documentation-website/Writerside/topics/Default-topic.md new file mode 100644 index 0000000000..ae3396669a --- /dev/null +++ b/documentation-website/Writerside/topics/Default-topic.md @@ -0,0 +1,80 @@ +# Welcome to Writerside! + + + +## Add new topics +You can create empty topics, or choose a template for different types of content that contains some boilerplate structure to help you get started: + +![Create new topic options](new_topic_options.png){ width=290 }{border-effect=line} + +## Write content +%product% supports two types of markup: Markdown and XML. +When you create a new help article, you can choose between two topic types, but this doesn't mean you have to stick to a single format. +You can author content in Markdown and extend it with semantic attributes or inject entire XML elements. + +## Inject XML +For example, this is how you inject a procedure: + + + +

Start typing and select a procedure type from the completion suggestions:

+ completion suggestions for procedure +
+ +

Press Tab or Enter to insert the markup.

+
+
+ +## Add interactive elements + +### Tabs +To add switchable content, you can make use of tabs (inject them by starting to type `tab` on a new line): + + + + ![Alt Text](new_topic_options.png){ width=450 } + + + + ]]> + + + +### Collapsible blocks +Apart from injecting entire XML elements, you can use attributes to configure the behavior of certain elements. +For example, you can collapse a chapter that contains non-essential information: + +#### Supplementary info {collapsible="true"} +Content under such header will be collapsed by default, but you can modify the behavior by adding the following attribute: +`default-state="expanded"` + +### Convert selection to XML +If you need to extend an element with more functions, you can convert selected content from Markdown to semantic markup. +For example, if you want to merge cells in a table, it's much easier to convert it to XML than do this in Markdown. +Position the caret anywhere in the table and press Alt+Enter: + +Convert table to XML + +## Feedback and support +Please report any issues, usability improvements, or feature requests to our +YouTrack project +(you will need to register). + +You are welcome to join our +public Slack workspace. +Before you do, please read our [Code of conduct](https://plugins.jetbrains.com/plugin/20158-writerside/docs/writerside-code-of-conduct.html). +We assume that you’ve read and acknowledged it before joining. + +You can also always send an email to [writerside@jetbrains.com](mailto:writerside@jetbrains.com). + + + + Markup reference + Reorder topics in the TOC + Build and publish + Configure Search + + diff --git a/documentation-website/Writerside/topics/Frequently-Asked-Questions.md b/documentation-website/Writerside/topics/Frequently-Asked-Questions.md new file mode 100644 index 0000000000..f4fbe3460c --- /dev/null +++ b/documentation-website/Writerside/topics/Frequently-Asked-Questions.md @@ -0,0 +1,133 @@ +# Frequently Asked Questions + +### Q: [Squash](https://github.com/orangy/squash) is same as Exposed. Where is the difference? +A: [Ilya Ryzhenkov](https://github.com/orangy/) (Squash maintainer) answers: +> Squash is an attempt to refactor Exposed (long time ago) to fix DSL issues, extensibility on dialect side, support graph fetching and avoid TLS-stored transactions. Unfortunately, I didn’t have enough time to finish the work, but I still hope to return to it some day. We are talking with Exposed maintainer [@tapac](https://github.com/orangy/) about coordinating efforts and eventually joining forces. Note, that none of these libs are “official” JetBrains Kotlin SQL libs, they are both side projects of their respective authors. + +### Q: Can I use multiple Database Connections? + +A: Yes. See [[Working with a multiple databases|Transactions#working-with-a-multiple-databases]] + +### Q: Is `Array` column type supported? + +A: Not at the moment. More info here: https://github.com/JetBrains/Exposed/issues/150 +The complete list of supported data types can be found here: [[Data Types|DataTypes]]. + +### Q: Is `upsert` supported? + +A: Yes. See [[Insert Or Update|DSL#insert-or-update]] + +### Q: Is `json` type supported? + +A: Yes. See [[How to use Json and JsonB types|DataTypes#how-to-use-json-and-jsonb-types]] + +### Q: How to get a plain SQL query which will be executed? + +A: +```kotlin +val plainSQL = FooTable.select {}.prepareSQL(QueryBuilder(false)) +``` +Use QueryBuiler with `false` - if you want to inline statement arguments, `true` - to see '?' in query. + +### Q: Is it possible to use native sql / sql as a string? + +A: It is not supported as part of the library but it is possible to implement on top of it and use it like this: +```kotlin +fun String.execAndMap(transform : (ResultSet) -> T) : List { + val result = arrayListOf() + TransactionManager.current().exec(this) { rs -> + while (rs.next()) { + result += transform(rs) + } + } + return result +} + +"select u.name, c.name from user u inner join city c where blah blah".execAndMap { rs -> + rs.getString("u.name") to rs.getString("c.name") +} +``` +More info in this issue: https://github.com/JetBrains/Exposed/issues/118 + +### Q: Is it possible to update a field relative to current field value? + +A: Yes. See example here: https://github.com/JetBrains/Exposed/wiki/DSL#update + +### Q: How can I add another type of Database? + +A: Implement `DatabaseDialect` interface and register it with `Database.registerDialect()`. +If the implementation adds a lot of value consider contributing it as a PR to Exposed. + +### Q: Is it possible to create tables with cross / cyclic reference? + +A: Yes, it's possible since Exposed 0.11.1 version + +### Q: How can I implement nested queries? + +A: See example here: https://github.com/JetBrains/Exposed/issues/248 + +### Q: How can I use SAVEPOINT? +A: It possible only through using a raw connection. See example [here](https://github.com/JetBrains/Exposed/issues/320#issuecomment-394825415). + +### Q: How to prepare query like: `SELECT * FROM table WHERE (x,y) IN ((1, 2), (3, 4), (5, 6))` +A: It possible with custom function. See [example](https://github.com/JetBrains/Exposed/issues/373#issuecomment-414123325). + +### Q: Where can I find snapshot builds of Exposed +A: You could use jitpack.io service for that. + +Add jitpack.io to repositories: +``` +repositories { + maven { url 'https://jitpack.io' } +} +``` +Then add Exposed dependency as stated below: +``` +dependencies { + implementation 'com.github.JetBrains:Exposed:-SNAPSHOT' +} +``` + +### Q: How can I specify a primary key column type e.g StringIdTable? +A: You need to define your own! See examples: +[#855](https://github.com/JetBrains/Exposed/issues/855) +https://stackoverflow.com/a/61940820/1155026 + +### Q: How create custom collumn type +A: Just implements [IColumnType](https://github.com/JetBrains/Exposed/blob/76a671e57a0105d6aed79e256c088690bd4a56b6/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt#L25) +and use [registerColumn](https://github.com/JetBrains/Exposed/blob/76a671e57a0105d6aed79e256c088690bd4a56b6/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt#L387) +to [extends](https://kotlinlang.org/docs/extensions.html) a [Table](https://github.com/JetBrains/Exposed/blob/76a671e57a0105d6aed79e256c088690bd4a56b6/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt#L326) + + +eg: **Create custom UUID types (inpired by [@pjagielski article](https://medium.com/@pjagielski/how-we-use-kotlin-with-exposed-at-touk-eacaae4565b5#e4e4))** +```kotlin +abstract class TypedId(open val id: UUID): Serializable, Comparable { + override fun compareTo(other: TypedId) = this.id.compareTo(other.id) +} + +class TypedUUIDColumnType(val constructor: (UUID) -> T, private val uuidColType: UUIDColumnType = UUIDColumnType()): IColumnType by uuidColType { + override fun valueFromDB(value: Any) = constructor(uuidColType.valueFromDB(value)) + override fun notNullValueToDB(value: Any): Any = uuidColType.notNullValueToDB(valueUnwrap(value)) + override fun nonNullValueToString(value: Any): String = uuidColType.nonNullValueToString(valueUnwrap(value)) + private fun valueUnwrap(value: Any) = (value as? TypedId)?.id ?: value +} + +fun Table.typedUuid(name: String, constructor: (UUID) -> T) = registerColumn(name, TypedUUIDColumnType(constructor)) +fun Column.autoGenerate(constructor: (UUID) -> T): Column = clientDefault { constructor(UUID.randomUUID()) } + + +class StarWarsFilmId(id: UUID): TypedId(id) + +object StarWarsFilms : Table() { + val id = typedUuid("id") { StarWarsFilmId(it) }.autoGenerate{ StarWarsFilmId(it) } + val name: Column = varchar("name", 50) + val director: Column = varchar("director", 50) + final override val primaryKey = PrimaryKey(id) +} +``` + + +Reference: [#149](https://github.com/JetBrains/Exposed/issues/149) + +### More questions on Stack Overflow: +[https://stackoverflow.com/questions/tagged/kotlin-exposed](https://stackoverflow.com/questions/tagged/kotlin-exposed) diff --git a/documentation-website/Writerside/topics/Functions.md b/documentation-website/Writerside/topics/Functions.md new file mode 100644 index 0000000000..6ab686ca1e --- /dev/null +++ b/documentation-website/Writerside/topics/Functions.md @@ -0,0 +1,192 @@ +# Functions + +Exposed provides basic support for classic SQL functions. This topic consists of definitions for those functions, and their +usage examples. It also explains how to define [custom functions](#custom-functions). + +## How to use functions +If you want to retrieve a function result from a query, you have to declare the function as a variable: +```kotlin +val lowerCasedName = FooTable.name.lowerCase() +val lowerCasedNames = FooTable.slice(lowerCasedName).selectAll().map { it[lowerCasedName] } + +``` +Also, functions could be chained and combined: +```kotlin +val trimmedAndLoweredFullName = Concat(FooTable.firstName, stringLiteral(" "), FooTable.lastName).trim().lowerCase() +val fullNames = FooTable.slice(trimmedAndLoweredFullName).selectAll().map { it[trimmedAndLoweredFullName] } + +``` + +## String functions +### LowerCase/UpperCase +Returns a lower-cased/upper-cased string value. +```kotlin +val lowerCasedName = FooTable.name.lowerCase() +val lowerCasedNames = FooTable.slice(lowerCasedName).selectAll().map { it[lowerCasedName] } + +``` +### Substring +Returns a substring value from the specified start and with the specified length. +```kotlin +val shortenedName = FooTable.name.substring(start = 1, length = 3) +val shortenedNames = FooTable.slice(shortenedName).selectAll().map { it[shortenedName] } + +``` +### Concat +Returns a string value that concatenates the text representations of all non-null input values, separated by an optional separator. +```kotlin +val userName = concat(stringLiteral("User - "), FooTable.name) +val userNames = FooTable.slice(userName).selectAll().map { it[userName] } + +``` +### Locate +Returns the index of the first occurrence of a specified substring or 0. +```kotlin +val firstAIndex = FooTable.name.locate("a") +val firstAIndices = FooTable.slice(firstAIndex).selectAll().map { it[firstAIndex] } + +``` +### CharLength +Returns the length, measured in characters, or `null` if the String value is null. +```kotlin +val nameLength = FooTable.name.charLength() +val nameLengths = FooTable.slice(nameLength).selectAll().map { it[nameLength] } + +``` + +## Aggregating functions +These functions should be used in queries with [[groupBy|DSL#group-by]]. +### Min/Max/Average +Returns minimum/maximum/average value and can be applied to any comparable expression: +```kotlin +val minId = FooTable.id.min() +val maxId = FooTable.id.max() +val averageId = FooTable.id.avg() +val (min, max, avg) = FooTable.slice(minId, maxId, averageId).selecAll().map { + Triple(it[minId], it[maxId], it[averageId]) +} + +``` + +## Custom functions +If you can't find your most loved function used in your database (as Exposed provides only basic support for classic SQL functions), you can define your own functions. + +Since Exposed 0.15.1 there multiple options to define custom functions: +1. Function without parameters: +```kotlin +val sqrt = FooTable.id.function("SQRT") +``` +In SQL representation it will be `SQRT(FooTable.id)` + +2. Function with additional parameters: +```kotlin +val replacedName = CustomFunction("REPLACE", VarCharColumnType(), FooTable.name, stringParam("foo"), stringParam("bar")) + +``` +`CustomFunction` class accepts a function name as a first parameter and the resulting column type as second. After that, you can provide any amount of parameters separated by a comma. + +There are also shortcuts for string, long, and datetime functions: +* `CustomStringFunction` +* `CustomLongFunction` +* `CustomDateTimeFunction` + +The code above could be simplified to: +```kotlin +val replacedName = CustomStringFunction("REPLACE", FooTable.name, stringParam("foo"), stringParam("bar")) + +``` +For example, the following could be used in SQLite to mimic its `date()` function: +```kotlin +val lastDayOfMonth = CustomDateFunction( + "date", + FooTable.dateColumn, + stringLiteral("start of month"), + stringLiteral("+1 month"), + stringLiteral("-1 day") +) +``` +3. Function that requires more complex query building: + +All functions in Exposed extend the abstract class `Function`, which takes a column type and allows overriding `toQueryBuilder()`. This is what `CustomFunction` actually does, which can be leveraged to create more complex queries. + +For example, Exposed provides a `trim()` function that removes leading and trailing whitespace from a String. In MySQL, this is just the default behavior as specifiers can be provided to limit the trim to either leading or trailing, as well as providing a specific substring other than spaces to remove. The custom function below supports this extended behavior: +```kotlin +enum class TrimSpecifier { BOTH, LEADING, TRAILING } + +class CustomTrim( + val expression: Expression, + val toRemove: Expression?, + val trimSpecifier: TrimSpecifier +) : Function(TextColumnType()) { + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + queryBuilder { + append("TRIM(") + append(trimSpecifier.name) + toRemove?.let { +" $it" } + append(" FROM ") + append(expression) + append(")") + } + } +} + +fun Expression.customTrim( + toRemove: Expression? = null, + specifier: TrimSpecifier = TrimSpecifier.BOTH +): CustomTrim = CustomTrim(this, toRemove, specifier) + +transaction { + FooTable.insert { it[name] = "xxxbarxxx" } + + val leadingXTrim = FooTable.name.customTrim(stringLiteral("x"), TrimSpecifier.LEADING) + val trailingXTrim = FooTable.name.customTrim(stringLiteral("x"), TrimSpecifier.TRAILING) + + FooTable.slice(leadingXTrim).selectAll() // barxxx + FooTable.slice(trailingXTrim).selectAll() // xxxbar +} + +``` + +## Window Functions + +Window functions allow calculations across a set of table rows that are related to the current row. + +Existing aggregate functions (like `sum()`, `avg()`) can be used, as well as new rank and value functions: +* `cumeDist()` +* `denseRank()` +* `firstValue()` +* `lag()` +* `lastValue()` +* `lead()` +* `nthValue()` +* `nTile()` +* `percentRank()` +* `rank()` +* `rowNumber()` + +To use a window function, include the `OVER` clause by chaining `.over()` after the function call. A `PARTITION BY` and +`ORDER BY` clause can be optionally chained using `.partitionBy()` and `.orderBy()`, which both take multiple arguments: +```kotlin +FooTable.amount.sum().over().partitionBy(FooTable.year, FooTable.product).orderBy(FooTable.amount) + +rowNumber().over().partitionBy(FooTable.year, FooTable.product).orderBy(FooTable.amount) + +FooTable.amount.sum().over().orderBy(FooTable.year to SortOrder.DESC, FooTable.product to SortOrder.ASC_NULLS_FIRST) +``` +Frame clause functions (like `rows()`, `range()`, and `groups()`) are also supported and take a `WindowFrameBound` option +depending on the expected result: +* `WindowFrameBound.currentRow()` +* `WindowFrameBound.unboundedPreceding()` +* `WindowFrameBound.unboundedFollowing()` +* `WindowFrameBound.offsetPreceding()` +* `WindowFrameBound.offsetFollowing()` +```kotlin +FooTable.amount.sum().over() + .partitionBy(FooTable.year, FooTable.product) + .orderBy(FooTable.amount) + .range(WindowFrameBound.offsetPreceding(2), WindowFrameBound.currentRow()) +``` + + +If multiple frame clause functions are chained together, only the last one will be used. + diff --git a/documentation-website/Writerside/topics/Getting-Started.md b/documentation-website/Writerside/topics/Getting-Started.md new file mode 100644 index 0000000000..8dbdaa57b7 --- /dev/null +++ b/documentation-website/Writerside/topics/Getting-Started.md @@ -0,0 +1,269 @@ +# Getting Started + +## Adding the Exposed dependency + + + + + + + mavenCentral + mavenCentral + https://repo1.maven.org/maven2/ + + + + + + org.jetbrains.exposed + exposed-core + 0.44.0 + + + org.jetbrains.exposed + exposed-dao + 0.44.0 + + + org.jetbrains.exposed + exposed-jdbc + 0.44.0 + + +]]> + + + + + + + + + +- Note: There are other modules. Detailed information is located in [Modules Documentation](Modules-Documentation.md) section. + +## Starting a transaction + +Every database access using Exposed is started by obtaining a connection and creating a transaction. + +To get a connection: + +```kotlin +Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") +``` + +It is also possible to provide `javax.sql.DataSource` for advanced behaviors such as connection pooling: + +```kotlin +Database.connect(dataSource) +``` + +More details in [Databases](Databases.md) + +After obtaining a connection, all SQL statements should be placed inside a transaction: + +```kotlin +transaction { + // Statements here +} +``` + +To see the actual database calls, add a logger: + +```kotlin +transaction { + // print SQL to stdout + addLogger(StdOutSqlLogger) +} +``` + +## Access Layers + +Exposed comes in two flavors: DSL and DAO. + +- DSL stands for Domain-Specific Language, and for Exposed it means type-safe syntax similar to SQL statements to access a database. For more + information, see [Deep Dive into DSL](Deep-Dive-into-DSL.md). + +- DAO means Data Access Object, and it allows treating the data from database as entities and performing CRUD operations. For more information, see + [Deep Dive into DAO](Deep-Dive-into-DAO.md). + +To get an idea of the difference, compare the following code samples and corresponding SQL outputs: + + + + + + + + + + = integer("population").uniqueIndex() +val country = Cities.customEnumeration( +"country", +"ENUM('RUSSIA', 'CHINA', 'USA', 'UNKNOWN')", +{ Country.values()[it as Int] }, +{ it.name } +).default(Country.UNKNOWN) +override val primaryKey = PrimaryKey(id, name = "Cities_ID") +} +class City(id: EntityID) : IntEntity(id) { +companion object : IntEntityClass(Cities) + + var name by Cities.name + var country by Cities.country + var population by Cities.population + +} + +enum class Country { +RUSSIA, CHINA, USA, UNKNOWN +} + +fun init() { +Database.connect( +"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", +driver = "org.h2.Driver" +) + + transaction { + // print sql calls to stdout + addLogger(StdOutSqlLogger) + + // create a table Cities + SchemaUtils.create(Cities) + + // insert a new city + val ny = City.new { + name = "New York" + country = Country.USA + population = 1000 + } + + // insert a new city + val mos = City.new { + name = "Moscow" + country = Country.RUSSIA + population = 500 + } + + // insert a new city + val stPet = City.new { + name = "St. Petersburg" + country = Country.RUSSIA + population = 300 + } + } + +} + +fun main() { +init() +} +]]> + + + + +SQL output: + + +SQL: SELECT VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE NAME = 'MODE' +SQL: CREATE TABLE IF NOT EXISTS CITIES ( + ID INT AUTO_INCREMENT, + "NAME" VARCHAR(50) DEFAULT 'Unknown' NOT NULL, + POPULATION INT NULL, + COUNTRY ENUM('RUSSIA', 'CHINA', 'USA') NULL, + CONSTRAINT Cities_ID PRIMARY KEY (ID) +) +SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) VALUES ('USA', 'New York', 1000) +SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) VALUES ('RUSSIA', 'Moscow', 500) +SQL: INSERT INTO CITIES (COUNTRY, "NAME", POPULATION) VALUES ('RUSSIA', 'St. Petersburg', 300) + + +More on [DSL](Deep-Dive-into-DSL.md) and [DAO](Deep-Dive-into-DAO.md). diff --git a/documentation-website/Writerside/topics/Introduction.md b/documentation-website/Writerside/topics/Introduction.md new file mode 100644 index 0000000000..6f331aed8e --- /dev/null +++ b/documentation-website/Writerside/topics/Introduction.md @@ -0,0 +1,24 @@ +# Introduction + +Welcome to the Exposed documentation! + +Exposed is a lightweight SQL library on top of JDBC driver for Kotlin. +It has two flavors of database access: type-safe SQL wrapping DSL (Domain-Specific Language) and lightweight DAO (Data Access Object). + +Exposed supports the following databases: + +* H2 +* MariaDB +* MySQL +* Oracle +* PostgreSQL +* SQL Server +* SQLite + +Exposed requires Java 6 or later versions. To check the version of Java you have installed, run the following command from the terminal: + +```shell +java -version +``` + +Exposed is an open-source project and is available on GitHub. diff --git a/documentation-website/Writerside/topics/Migration-Guide.md b/documentation-website/Writerside/topics/Migration-Guide.md new file mode 100644 index 0000000000..aa6c16bc0f --- /dev/null +++ b/documentation-website/Writerside/topics/Migration-Guide.md @@ -0,0 +1,3 @@ +# Migration Guide + +Start typing here... \ No newline at end of file diff --git a/documentation-website/Writerside/topics/Modules-Documentation.md b/documentation-website/Writerside/topics/Modules-Documentation.md new file mode 100644 index 0000000000..04611fed7b --- /dev/null +++ b/documentation-website/Writerside/topics/Modules-Documentation.md @@ -0,0 +1,213 @@ +# Modules Documentation + +## Dependencies + +Exposed modules are available from Maven Central repository. +To use them you have to add appropriate dependency into your repositories mapping. + +#### Maven + +```xml + + + + + mavenCentral + mavenCentral + https://repo1.maven.org/maven2/ + + +``` + +#### Gradle Groovy and Kotlin DSL + +```kotlin +repositories { + // Versions after 0.30.1 + // Versions before 0.30.1 is unavailable for now + mavenCentral() +} +``` + +## Base Modules + +### Exposed 0.17.x and lower + +Prior Exposed 0.18.1 there was only one base module `exposed` which contains everything you may need including JodaTime as date-time library. +To add `Exposed` framework of that version you had to use: + +#### Maven + +```xml + + + + org.jetbrains.exposed + exposed + 0.17.7 + + + +``` + +#### Gradle Groovy + +```groovy +dependencies { + implementation 'org.jetbrains.exposed:exposed:0.17.7' +} +``` + +#### Gradle Kotlin DSL + +```kotlin +dependencies { + implementation("org.jetbrains.exposed", "exposed", "0.17.7") +} +``` + +### Exposed 0.18.1 and higher + +To move forward and support such features as Java 8 Time, async drivers, and so on, it was decided to split Exposed into more specific modules. It will allow you to +take the only modules you need and will add flexibility in the future. + +`Exposed` consists of the following modules: + +* exposed-core - base module, which contains both DSL api along with mapping +* exposed-crypt - provides additional column types to store encrypted data in DB and encode/decode it on client-side +* exposed-dao - DAO api +* exposed-java-time - date-time extensions based on Java8 Time API +* exposed-jdbc - transport level implementation based on Java JDBC API +* exposed-jodatime - date-time extensions based on JodaTime library +* exposed-json - JSON and JSONB data type extensions +* exposed-kotlin-datetime - date-time extensions based on kotlinx-datetime +* exposed-money - extensions to support MonetaryAmount from "javax.money:money-api" +* exposed-spring-boot-starter - a starter for [Spring Boot](https://spring.io/projects/spring-boot) to utilize Exposed as the ORM instead + of [Hibernate](https://hibernate.org/) + +Dependencies mapping listed below is similar (by functionality) to the previous versions: + +#### Maven + +```xml + + + + org.jetbrains.exposed + exposed-core + 0.44.0 + + + org.jetbrains.exposed + exposed-crypt + 0.44.0 + + + org.jetbrains.exposed + exposed-dao + 0.44.0 + + + org.jetbrains.exposed + exposed-java-time + 0.44.0 + + + org.jetbrains.exposed + exposed-jdbc + 0.44.0 + + + org.jetbrains.exposed + exposed-jodatime + 0.44.0 + + + org.jetbrains.exposed + exposed-json + 0.44.0 + + + org.jetbrains.exposed + exposed-kotlin-datetime + 0.44.0 + + + org.jetbrains.exposed + exposed-money + 0.44.0 + + + org.jetbrains.exposed + exposed-spring-boot-starter + 0.44.0 + + + +``` + +#### Gradle Groovy + +```groovy +dependencies { + implementation 'org.jetbrains.exposed:exposed-core:0.44.0' + implementation 'org.jetbrains.exposed:exposed-crypt:0.44.0' + implementation 'org.jetbrains.exposed:exposed-dao:0.44.0' + implementation 'org.jetbrains.exposed:exposed-jdbc:0.44.0' + + implementation 'org.jetbrains.exposed:exposed-jodatime:0.44.0' + // or + implementation 'org.jetbrains.exposed:exposed-java-time:0.44.0' + // or + implementation 'org.jetbrains.exposed:exposed-kotlin-datetime:0.44.0' + + implementation 'org.jetbrains.exposed:exposed-json:0.44.0' + implementation 'org.jetbrains.exposed:exposed-money:0.44.0' + implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0' +} +``` + +#### Gradle Kotlin DSL + +In `build.gradle.kts`: + +```kotlin +val exposedVersion: String by project +dependencies { + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-crypt:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") + + implementation("org.jetbrains.exposed:exposed-jodatime:$exposedVersion") + // or + implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") + // or + implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion") + + implementation("org.jetbrains.exposed:exposed-json:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-money:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion") +} +``` + +and in `gradle.properties` + +``` +exposedVersion=0.44.0 +``` + +### JDBC driver and logging + +You also need a JDBC driver for the database system you are using (see [Databases](Databases.md)) and a logger for `addLogger(StdOutSqlLogger)`. Example (Gradle +syntax): + +```kotlin +dependencies { + // for H2 + implementation("com.h2database:h2:2.1.214") + // for logging (StdOutSqlLogger), see + // http://www.slf4j.org/codes.html#StaticLoggerBinder + implementation("org.slf4j:slf4j-nop:1.7.30") +} +``` diff --git a/documentation-website/Writerside/topics/Samples.md b/documentation-website/Writerside/topics/Samples.md new file mode 100644 index 0000000000..49d4c8e5b8 --- /dev/null +++ b/documentation-website/Writerside/topics/Samples.md @@ -0,0 +1,3 @@ +# Samples + +Samples can be found [here](https://github.com/JetBrains/Exposed/tree/main/samples). diff --git a/documentation-website/Writerside/topics/Schemas.md b/documentation-website/Writerside/topics/Schemas.md new file mode 100644 index 0000000000..15e4f5237d --- /dev/null +++ b/documentation-website/Writerside/topics/Schemas.md @@ -0,0 +1,7 @@ +# Schemas + +[Table Definition](Table-Definition.md) + +[Data Types](Data-Types.md) + +[Functions](Functions.md) diff --git a/documentation-website/Writerside/topics/Table-Definition.md b/documentation-website/Writerside/topics/Table-Definition.md new file mode 100644 index 0000000000..9a03061be2 --- /dev/null +++ b/documentation-website/Writerside/topics/Table-Definition.md @@ -0,0 +1,196 @@ +# Table Definition + +This topic shows what table types Exposed supports and how to define and create tables. Also, it contains tips on configuring +constraints, such as `PRIMARY KEY`, `DEFAULT`, `INDEX` and so on. + +## Table Types + +The most primitive table type is `Table()`. It's located in **org.jetbrains.exposed.sql** package and has `NOT NULL` SQL constraint +configured by default on all columns. To configure a custom name for the table, which will be used in actual SQL queries, pass +it to the `name` parameter of the `Table()` constructor. Otherwise, Exposed will generate it from a class name. + +For example, to create a simple table called `"citiesTable"` with integer `id` column and string `name` column, use the +following code. +```kotlin +// SQL: CREATE TABLE IF NOT EXISTS CITIESTABLE ( +// ID INT NOT NULL, +// "NAME" VARCHAR(50) NOT NULL +// ) +object Cities : Table(name = "citiesTable") { + val id = integer("id") + val name = varchar("name", 50) +} +``` +Also, Exposed provides `IdTable` class which is inherited by `IntIdTable()`, `LongIdTable()`, and `UUIDTable(`) classes from +**org.jetbrains.exposed.dao.id** package of **exposed-core** module. These tables could be declared without the `id` attribute. +IDs of appropriate type will be generated automatically when creating new table rows. To configure a custom name +for the `id` attribute, pass it to the `columnName` parameter of the appropriate table constructor. + +Depending on what DBMS you use, types of columns could be different in actual SQL queries. We use H2 database in our examples. + +## Constraints + +### Nullable + +The `NOT NULL` SQL constraint restricts the column to accept the `null` value. By default, Exposed applies this constraint to +all the columns. To allow the column to be nullable, apply the `nullable()` method to a definition of an appropriate column. + +For example, to make the population column `nullable`, use the following code: +```kotlin +// SQL: POPULATION INT NULL +val population: Column = integer("population").nullable() +``` + +### Default + +The `DEFAULT` SQL constraint provides the default value for the column. Exposed supports three methods for configuring +default values: + +* `default(defaultValue: T)` accepts a value with a type of the column. +* `defaultExpression(defaultValue: Expression)` accepts an expression. +* `clientDefault(defaultValue: () -> T)` accepts a function. + +For example, to configure the default value for the `name` column, use the following code: +```kotlin +// SQL: "NAME" VARCHAR(50) DEFAULT 'Unknown' +val name: Column = varchar("name", 50).default("Unknown") +``` + +### Index + +The `INDEX` SQL constraint makes traversing through tables quicker. Exposed supports the `index()` method. +It has six parameters, most of which are optional: + +* `val customIndexName: String? = null` is a custom name for the index, which will be used in actual SQL queries. +* `val unique: Boolean` defines whether the index is unique or not. +* `val columns: List>` defines a column set. +* `val functions: List>? = null` defines functional key parts. +* `val indexType: String? = null` is a custom type. Can be `"BTREE"` or `"HASH"`. +* `val filterCondition: (SqlExpressionBuilder.() -> Op)? = null` defines a condition used to create a partial index. + +The simplest way to create an index is to use an extension function directly on a column. For example, to apply a non-unique +`INDEX` constraint to the `name` column, use the following code: +```kotlin +val name = varchar("name", 50).index() +``` +If the parameter `customIndexName` is not set, the name of the index is determined by the table and column names. + +Also, Exposed supports complex indexes. If you have a frequent query for two columns, Exposed can perform it more efficiently. +It creates a tree from the first column with the references to the second one. For example, to create a non-unique complex +index on the `name` and `population` columns, paste the following code: +```kotlin +val indexName = index("indexName", false, *arrayOf(name, population)) +// or inside an init block within the table object +init { + index("indexName", isUnique = false, name, population) +} +``` + +Exposed also supports creating an index with a custom type. For example, to retrieve data from the `name` column faster +with a hash function for traversing, use the following code: +```kotlin +val indexName = index("indexName", false, *arrayOf(name), indexType = "HASH") +``` + +Some databases support functional key parts that index expressions instead of columns directly: +```kotlin +init { + index(functions = listOf(name.lowerCase(), address.substring(1, 5))) + uniqueIndex(columns = arrayOf(name), functions = listOf(Coalesce(address, stringLiteral("*")))) +} +``` +Operator expressions, like `plus()`, are also accepted by the `functions` parameter. + +Some databases support creating a partial index by defining a filter expression to improve querying performance. The +created index will only contain entries for the table rows that match this predicate: +```kotlin +init { + index(columns = arrayOf(name, flag)) { flag eq true } + index(columns = arrayOf(name, population)) { (name like "A%") and (population greaterEq 10) } +} +``` + +Once a table has been created, the list of its indices can be accessed using the property `Table.indices`. Table indices +are represented by the data class `Index`, so its properties can be checked in the following manner, for example: +```kotlin +Table.indices.map { it.indexName to it.createStatement().first() } +``` + + +An instance of the `Index` data class can be created directly using its public constructor, for the purpose of +evaluating or using create/modify/drop statements, for example. Doing so will not add the instance to an existing table's +list of indices in the way that using `index()` would. Also, if an instance is created with arguments provided to the +`functions` parameter, a `functionsTable` argument must also be provided. + + +### Unique + +The `UNIQUE` SQL constraint restricts duplicates within this column. Exposed supports the `uniqueIndex()` method which +creates a unique index for the column. This method is the composition of `UNIQUE` and `INDEX` constraint, the quicker +modification of `UNIQUE` constraint. + +For example, to apply `UNIQUE` and `INDEX` constraint to the `name` column, use the following code: +```kotlin +val name = varchar("name", 50).uniqueIndex() +``` + +### Primary Key + +The `PRIMARY KEY` SQL constraint applied to columns means each value in a column identifies the row. It's the composition +of `NOT NULL` and `UNIQUE` constraints. Each kind of table in Exposed, inherited from `IdTable()`, has the `primaryKey` +field. For example, the `IntIdTable` has default integer `id` primary key. If you want to change column set, add columns, +or change primary key name to a custom one, you need to override this field of the appropriate table class. + +For example, if you want to define the `name` column as a primary key, use the following code. The "Cities_name" string +will be used in actual SQL query. +```kotlin +// SQL: CONSTRAINT Cities_name PRIMARY KEY ("NAME") +override val primaryKey = PrimaryKey(name, name = "Cities_name") +``` + +### Foreign Key + +The `FOREIGN KEY` SQL constraint links two tables. Foreign key is a column from one table that refers to the primary key +or columns with a unique index from another table. To configure the foreign key, use `reference()` or `optReference()` +method. The second one let the foreign key accept the `null` value. + +`reference()` and `optReference()` methods have several parameters: + +* `name: String` is a name for foreign key column, which will be used in actual SQL queries. +* `ref: Column` is a target column from another parent table. +* `onDelete: ReferenceOption? = null` is an action to the case when linked row from a parent table can be deleted. +* `onUpdate: ReferenceOption? = null` is an action to the case when value in a referenced column can be changed. +* `fkName: String? = null` is a foreign key constraint name. + +Enum class `ReferenceOption` has four values: + +* `RESTRICT` is an option that restricts changes on a referenced column, and the default option for MySQL dialect. +* `NO_ACTION` is the same as RESTRICT, and the default option for Oracle and SQL Server dialects. +* `CASCADE` is an option that allows updating or deleting the referring rows. +* `SET_NULL` is an option that sets the referring column values to null. +* `SET_DEFAULT` is an option that sets the referring column values to the default value. + +Consider the following `Citizens` table. This table has the `name` and `city` columns. Since the `Cities` table has +configured `name` primary key, the `Citizens` table can refer to it by its `city` column, which is a foreign key. To +configure such reference and make it nullable, use the `optReference()` method: +```kotlin +object Citizens : IntIdTable() { + val name = varchar("name", 50) + val city = optReference("city", Cities.name, onDelete=ReferenceOption.CASCADE) +} +``` + +If any `Cities` row will be deleted, the appropriate `Citizens` row will be deleted too. + +### Check + +The `CHECK` SQL constraint checks that all values in a column match some condition. Exposed supports the `check()` method. +You apply this method to a column and pass the appropriate condition to it. + +For example, to check that the `name` column contains strings that begin with a capital letter, use the following code: +```kotlin +// SQL: CONSTRAINT check_Cities_0 CHECK (REGEXP_LIKE("NAME", '^[A-Z].*', 'c'))) +val name = varchar("name", 50).check { it regexp "^[A-Z].*" } +``` + +Some databases, like older MySQL versions, may not support `CHECK` constraints. For more information, consult the relevant documentation. diff --git a/documentation-website/Writerside/topics/Tutorials-and-Samples.md b/documentation-website/Writerside/topics/Tutorials-and-Samples.md new file mode 100644 index 0000000000..aef50b7579 --- /dev/null +++ b/documentation-website/Writerside/topics/Tutorials-and-Samples.md @@ -0,0 +1,7 @@ +# Tutorials and Samples + +This section will demonstrate how to get started with Exposed. + +[Getting Started](Getting-Started.md) + +[Samples](Samples.md) diff --git a/documentation-website/Writerside/topics/Working-with-DataSource.md b/documentation-website/Writerside/topics/Working-with-DataSource.md new file mode 100644 index 0000000000..58bdb4bfe1 --- /dev/null +++ b/documentation-website/Writerside/topics/Working-with-DataSource.md @@ -0,0 +1,31 @@ +# Working with DataSource + +It is also possible to provide a `javax.sql.DataSource` to the `Database.connect` function. This allows you to use more advanced features like +connection pooling, and lets you set configuration options like maximum number of connections, connection timeouts, etc. + +```kotlin +val db = Database.connect(dataSource) +``` + +## MariaDB/MySQL with latest JDBC driver + Hikari pooling + +Add dependency: + +```kotlin +implementation("mysql:mysql-connector-java:8.0.19") +implementation("com.zaxxer:HikariCP:3.4.2") +``` + +Connect to database: + +```kotlin +val config = HikariConfig().apply { + jdbcUrl = "jdbc:mysql://localhost/dbname" + driverClassName = "com.mysql.cj.jdbc.Driver" + username = "user" + password = "password" + maximumPoolSize = 10 +} +val dataSource = HikariDataSource(config) +Database.connect(dataSource) +``` diff --git a/documentation-website/Writerside/topics/Working-with-Database.md b/documentation-website/Writerside/topics/Working-with-Database.md new file mode 100644 index 0000000000..16c8d8a1e2 --- /dev/null +++ b/documentation-website/Writerside/topics/Working-with-Database.md @@ -0,0 +1,181 @@ +# Working with Database + +In Exposed, the `Database` class represents a database instance, and encapsulates the necessary connection +details and configuration required to interact with a specific database. + +## Connecting to a Database + +To connect to a database using `Database`, you need to provide the appropriate JDBC driver and connection URL. Here's an example of how to establish a +connection: + +```kotlin +val db = Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") +``` + +The `Database.connect` function tells Exposed _how_ to connect to a database, but won't _create_ a database connection. It only provides a +descriptor for future usage. A connection will be created later by calling the `transaction` lambda (see [Transaction](Working-with-Transaction.md) for more +details). + + +Starting from Exposed 0.10, executing this code more than once per database will create leaks in your application; hence, it is recommended to + store it for later use. + + +Creating a database only when it is accessed for the first time can be done like this: + +```kotlin +object DbSettings { + val db by lazy { + Database.connect(/* setup connection */) + } +} +``` + +### H2 + +In order to use H2, you need to add the H2 driver dependency: + +```kotlin +implementation("com.h2database:h2:2.1.214") +``` + +Then connect to a database: + +```kotlin +Database.connect("jdbc:h2:./myh2file", "org.h2.Driver") +``` + +Or in-memory database: + +```kotlin +Database.connect("jdbc:h2:mem:regular", "org.h2.Driver") +``` + +By default, H2 closes the database when the last connection is closed. If you want to keep the database open, you can use the `DB_CLOSE_DELAY=-1` +option: + +```kotlin +Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver") +``` + +### MariaDB/MySQL + +Add dependency: + +```kotlin +implementation("mysql:mysql-connector-java:8.0.2") +``` + +Connect to database: + +```kotlin +Database.connect( + "jdbc:mysql://localhost:3306/test", + driver = "com.mysql.cj.jdbc.Driver", + user = "user", + password = "password" +) +``` + +### Oracle + +Add dependency: + +```kotlin +implementation("com.oracle.database.jdbc:ojdbc8:12.2.0.1") +``` + +Connect to database: + +```kotlin +Database.connect( + "jdbc:oracle:thin:@//localhost:1521/test", + driver = "oracle.jdbc.OracleDriver", + user = "user", + password = "password" +) +``` + +### PostgreSQL + +Add dependency: + +```kotlin +implementation("org.postgresql:postgresql:42.2.2") +``` + +Connect to database: + +```kotlin +Database.connect( + "jdbc:postgresql://localhost:12346/test", + driver = "org.postgresql.Driver", + user = "user", + password = "password" +) +``` + +### PostgreSQL using the pgjdbc-ng JDBC driver + +Add dependency: + +```kotlin +implementation("com.impossibl.pgjdbc-ng", "pgjdbc-ng", "0.8.3") +``` + +Connect to database: + +```kotlin +Database.connect( + "jdbc:pgsql://localhost:12346/test", + driver = "com.impossibl.postgres.jdbc.PGDriver", + user = "user", + password = "password" +) +``` + +### SQL Server + +Add dependency: + +```kotlin +implementation("com.microsoft.sqlserver:mssql-jdbc:6.4.0.jre7") +``` + +Connect to database: + +```kotlin +Database.connect( + "jdbc:sqlserver://localhost:32768;databaseName=test", + "com.microsoft.sqlserver.jdbc.SQLServerDriver", + user = "user", + password = "password" +) +``` + +### SQLite + +Add the dependency: + +```kotlin +implementation("org.xerial:sqlite-jdbc:3.30.1") +``` + +Connect to database: + +```kotlin +Database.connect("jdbc:sqlite:/data/data.db", "org.sqlite.JDBC") +``` + +Or in-memory database: + +```kotlin +Database.connect("jdbc:sqlite:file:test?mode=memory&cache=shared", "org.sqlite.JDBC") +``` + +For both: set SQLite compatible isolation level: [FAQ](Frequently-Asked-Questions.md). + +```kotlin +TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE +// or Connection.TRANSACTION_READ_UNCOMMITTED +``` diff --git a/documentation-website/Writerside/topics/Working-with-Transaction.md b/documentation-website/Writerside/topics/Working-with-Transaction.md new file mode 100644 index 0000000000..a294bcbc2c --- /dev/null +++ b/documentation-website/Writerside/topics/Working-with-Transaction.md @@ -0,0 +1,140 @@ +# Working with Transaction + +## Overview + +CRUD operations in Exposed must be called from within a _transaction._ Transactions encapsulate a set of DSL operations. To create and execute a +transaction with default parameters, simply pass a function block to the `transaction` function: + +```kotlin +transaction { + // DSL/DAO operations go here +} +``` + +Transactions are executed synchronously on the current thread, so they _will block_ other parts of your application! If you need to execute a +transaction asynchronously, consider running it on a [separate thread](Asynchronous-Support.md). + +### Accessing returned values + +Although you can modify variables from your code within the `transaction` block, `transaction` supports returning a value directly, enabling +immutability: + +```kotlin +val jamesList = transaction { + Users.select { Users.firstName eq "James" }.toList() +} +// jamesList is now a List containing Users data +``` + + +`Blob` and `text` fields won't be available outside of a transaction if you don't load them directly. For `text` fields you can also use +the `eagerLoading` param when defining the Table to make the text fields available outside of the transaction. + + +```kotlin +// without eagerLoading +val idsAndContent = transaction { + Documents.selectAll().limit(10).map { it[Documents.id] to it[Documents.content] } +} + +// with eagerLoading for text fields +object Documents : Table() { + val content = text("content", eagerLoading = true) +} + +val documentsWithContent = transaction { + Documents.selectAll().limit(10) +} +``` + +### Working with multiple databases + +If you want to work with different databases, you have to store the database reference returned by `Database.connect()` and provide it +to `transaction` function as the first parameter. The `transaction` block without parameters will work with the latest connected database. + +```kotlin +val db1 = connect("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;", "org.h2.Driver", "root", "") +val db2 = connect("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1;", "org.h2.Driver", "root", "") +transaction(db1) { + val result = transaction(db2) { + Table1.select { }.map { it[Table1.name] } + } + + val count = Table2.select { Table2.name inList result }.count() +} +``` + +Entities (see [DAO](Deep-Dive-into-DAO.md) page) _stick_ to the transaction that was used to load that entity. That means that all changes persist to the same +database and cross-database references are prohibited and will throw exceptions. + +### Setting default database + +The `transaction` block without parameters will use the default database. +Before 0.10.1, this will be the latest _connected_ database. +It is also possible to set the default database explicitly. + +```kotlin +val db = Database.connect() +TransactionManager.defaultDatabase = db +``` + +### Using nested transactions + +Since Exposed 0.16.1, it is possible to use nested transactions. To enable this feature, you should set `useNestedTransactions` on the desired `Database` +instance to `true`. + +After that, any exception that happens within the `transaction` block will not rollback the whole transaction but only the code inside the +current `transaction`. +Exposed uses SQL `SAVEPOINT` functionality to mark the current transaction at the beginning of the `transaction` block and release it on exit from it. + +Using `SAVEPOINT` could affect performance, so please read the documentation of the DBMS you use for more details. + +```kotlin +val db = Database.connect() +db.useNestedTransactions = true + +transaction { + FooTable.insert { it[id] = 1 } + + var idToInsert = 0 + transaction { // nested transaction + idToInsert ++ + // On the first insert it will fail with unique constraint exception and will rollback to the `nested transaction` and then insert a new record with id = 2 + FooTable.insert { it[id] = idToInsert } + } +} +``` + +### Advanced parameters and usage + +For specific functionality, transactions can be created with additional parameters: `transactionIsolation` and `db`. +The following properties can be set inside the `transaction` lambda: + +* `repetitionAttempts`: The number of retries that will be made inside this `transaction` block if SQLException happens +* `minRepetitionDelay`: The minimum number of milliseconds to wait before retrying this `transaction` if SQLException happens +* `maxRepetitionDelay`: The maximum number of milliseconds to wait before retrying this `transaction` if SQLException happens + +```kotlin +transaction(Connection.TRANSACTION_SERIALIZABLE, db) { + repetitionAttempts = 2 + // DSL/DAO operations go here +} +``` + +**Transaction Isolation**: This parameter, defined in the SQL standard, specifies what is supposed to happen when multiple transactions execute +concurrently on the database. This value does NOT affect Exposed operations directly, but is sent to the database, where it is expected to be obeyed. +Allowable values are defined in `java.sql.Connection` and are as follows: + +* **TRANSACTION_NONE**: Transactions are not supported. +* **TRANSACTION_READ_UNCOMMITTED**: The most lenient setting. Allows uncommitted changes from one transaction to affect a read in another + transaction (a "dirty read"). +* **TRANSACTION_READ_COMMITTED**: This setting prevents dirty reads from occurring, but still allows non-repeatable reads to occur. A _non-repeatable + read_ is when a transaction ("Transaction A") reads a row from the database, another transaction ("Transaction B") changes the row, and Transaction + A reads the row again, resulting in an inconsistency. +* **TRANSACTION_REPEATABLE_READ**: The default setting for Exposed transactions. Prevents both dirty and non-repeatable reads, but still allows for + phantom reads. A _phantom read_ is when a transaction ("Transaction A") selects a list of rows through a `WHERE` clause, another transaction (" + Transaction B") performs an `INSERT` or `DELETE` with a row that satisfies Transaction A's `WHERE` clause, and Transaction A selects using the same + WHERE clause again, resulting in an inconsistency. +* **TRANSACTION_SERIALIZABLE**: The strictest setting. Prevents dirty reads, non-repeatable reads, and phantom reads. + +**db** parameter is optional and used to select the database where the transaction should be settled (see section above). diff --git a/documentation-website/Writerside/v.list b/documentation-website/Writerside/v.list new file mode 100644 index 0000000000..bdf1b0e432 --- /dev/null +++ b/documentation-website/Writerside/v.list @@ -0,0 +1,5 @@ + + + + + diff --git a/exposed-bom/README.md b/exposed-bom/README.md index b910f1a233..42765d8d9b 100644 --- a/exposed-bom/README.md +++ b/exposed-bom/README.md @@ -17,7 +17,7 @@ Bill of Materials for all Exposed modules org.jetbrains.exposed exposed-bom - 0.41.1 + 0.44.0 pom import @@ -46,12 +46,12 @@ Bill of Materials for all Exposed modules # Gradle ```kotlin repositories { - // Versions after 0.33.1 - mavenCentral() + // Versions after 0.33.1 + mavenCentral() } dependencies { - implementation(platform("org.jetbrains.exposed:exposed-bom:0.41.1")) + implementation(platform("org.jetbrains.exposed:exposed-bom:0.44.0")) implementation("org.jetbrains.exposed", "exposed-core") implementation("org.jetbrains.exposed", "exposed-dao") implementation("org.jetbrains.exposed", "exposed-jdbc") diff --git a/exposed-bom/build.gradle.kts b/exposed-bom/build.gradle.kts index 8d75683368..f9e78b7d65 100644 --- a/exposed-bom/build.gradle.kts +++ b/exposed-bom/build.gradle.kts @@ -31,8 +31,13 @@ dependencies { } publishing { + val version: String by rootProject + publications { create("bom") { + groupId = "org.jetbrains.exposed" + artifactId = project.name + this.version = version from(components.getByName("javaPlatform")) pom { configureMavenCentralMetadata(project) @@ -40,4 +45,18 @@ publishing { 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/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api new file mode 100644 index 0000000000..8523674d6f --- /dev/null +++ b/exposed-core/api/exposed-core.api @@ -0,0 +1,3609 @@ +public class org/jetbrains/exposed/dao/id/EntityID : java/lang/Comparable { + public fun (Ljava/lang/Comparable;Lorg/jetbrains/exposed/dao/id/IdTable;)V + protected fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Comparable;)V + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo (Lorg/jetbrains/exposed/dao/id/EntityID;)I + public fun equals (Ljava/lang/Object;)Z + public final fun getTable ()Lorg/jetbrains/exposed/dao/id/IdTable; + public final fun getValue ()Ljava/lang/Comparable; + public final fun get_value ()Ljava/lang/Object; + public fun hashCode ()I + protected fun invokeOnNoValue ()V + public final fun set_value (Ljava/lang/Object;)V + public fun toString ()Ljava/lang/String; +} + +public abstract interface class org/jetbrains/exposed/dao/id/EntityIDFactory { + public abstract fun createEntityID (Ljava/lang/Comparable;Lorg/jetbrains/exposed/dao/id/IdTable;)Lorg/jetbrains/exposed/dao/id/EntityID; +} + +public final class org/jetbrains/exposed/dao/id/EntityIDFunctionProvider { + public static final field INSTANCE Lorg/jetbrains/exposed/dao/id/EntityIDFunctionProvider; + public final fun createEntityID (Ljava/lang/Comparable;Lorg/jetbrains/exposed/dao/id/IdTable;)Lorg/jetbrains/exposed/dao/id/EntityID; +} + +public abstract class org/jetbrains/exposed/dao/id/IdTable : org/jetbrains/exposed/sql/Table { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public abstract fun getId ()Lorg/jetbrains/exposed/sql/Column; +} + +public class org/jetbrains/exposed/dao/id/IntIdTable : org/jetbrains/exposed/dao/id/IdTable { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getId ()Lorg/jetbrains/exposed/sql/Column; + public final fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; +} + +public class org/jetbrains/exposed/dao/id/LongIdTable : org/jetbrains/exposed/dao/id/IdTable { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getId ()Lorg/jetbrains/exposed/sql/Column; + public final fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; +} + +public class org/jetbrains/exposed/dao/id/UUIDTable : org/jetbrains/exposed/dao/id/IdTable { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getId ()Lorg/jetbrains/exposed/sql/Column; + public final fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; +} + +public final class org/jetbrains/exposed/exceptions/DuplicateColumnException : java/lang/ExceptionInInitializerError { + public fun (Ljava/lang/String;Ljava/lang/String;)V +} + +public final class org/jetbrains/exposed/exceptions/ExposedSQLException : java/sql/SQLException { + public fun (Ljava/lang/Throwable;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;)V + public final fun causedByQueries ()Ljava/util/List; + public final fun getContexts ()Ljava/util/List; + public fun getErrorCode ()I + public fun getSQLState ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/exceptions/LongQueryException : java/lang/RuntimeException { + public fun ()V +} + +public final class org/jetbrains/exposed/exceptions/UnsupportedByDialectException : java/lang/UnsupportedOperationException { + public fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)V + public final fun getDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; +} + +public abstract class org/jetbrains/exposed/sql/AbstractQuery : org/jetbrains/exposed/sql/statements/Statement, org/jetbrains/exposed/sql/SizedIterable { + public static final field Companion Lorg/jetbrains/exposed/sql/AbstractQuery$Companion; + public fun (Ljava/util/List;)V + public synthetic fun arguments ()Ljava/lang/Iterable; + public fun arguments ()Ljava/util/List; + protected final fun copyTo (Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public final fun fetchSize (I)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + protected final fun getCount ()Z + public final fun getFetchSize ()Ljava/lang/Integer; + public final fun getLimit ()Ljava/lang/Integer; + public final fun getOffset ()J + public final fun getOrderByExpressions ()Ljava/util/List; + protected abstract fun getQueryToExecute ()Lorg/jetbrains/exposed/sql/statements/Statement; + public abstract fun getSet ()Lorg/jetbrains/exposed/sql/FieldSet; + protected final fun getTransaction ()Lorg/jetbrains/exposed/sql/Transaction; + public fun iterator ()Ljava/util/Iterator; + public fun limit (IJ)Lorg/jetbrains/exposed/sql/AbstractQuery; + public synthetic fun limit (IJ)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun orderBy (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/SortOrder;)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/AbstractQuery; + public synthetic fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/SizedIterable; + public static synthetic fun orderBy$default (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/SortOrder;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/AbstractQuery; + public abstract fun prepareSQL (Lorg/jetbrains/exposed/sql/QueryBuilder;)Ljava/lang/String; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; + protected final fun setCount (Z)V + protected final fun setLimit (Ljava/lang/Integer;)V + public abstract fun withDistinct (Z)Lorg/jetbrains/exposed/sql/AbstractQuery; + public static synthetic fun withDistinct$default (Lorg/jetbrains/exposed/sql/AbstractQuery;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/AbstractQuery; +} + +public final class org/jetbrains/exposed/sql/AbstractQuery$Companion { +} + +public final class org/jetbrains/exposed/sql/Alias : org/jetbrains/exposed/sql/Table { + public fun (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)V + public fun createStatement ()Ljava/lang/Void; + public synthetic fun createStatement ()Ljava/util/List; + public fun dropStatement ()Ljava/lang/Void; + public synthetic fun dropStatement ()Ljava/util/List; + public fun equals (Ljava/lang/Object;)Z + public final fun get (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun getAlias ()Ljava/lang/String; + public fun getColumns ()Ljava/util/List; + public final fun getDelegate ()Lorg/jetbrains/exposed/sql/Table; + public fun getFields ()Ljava/util/List; + public fun getTableName ()Ljava/lang/String; + public final fun getTableNameWithAlias ()Ljava/lang/String; + public fun hashCode ()I + public fun modifyStatement ()Ljava/lang/Void; + public synthetic fun modifyStatement ()Ljava/util/List; + public final fun originalColumn (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/AliasKt { + public static final fun alias (Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/QueryAlias; + public static final fun alias (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ExpressionAlias; + public static final fun alias (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Alias; + public static final fun getLastQueryAlias (Lorg/jetbrains/exposed/sql/Join;)Lorg/jetbrains/exposed/sql/QueryAlias; + public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join; + public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun joinQuery$default (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun joinQuery$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun wrapAsExpression (Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/AndBitOp : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/AndOp : org/jetbrains/exposed/sql/CompoundBooleanOp { + public fun (Ljava/util/List;)V +} + +public final class org/jetbrains/exposed/sql/AutoIncColumnType : org/jetbrains/exposed/sql/IColumnType { + public fun (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getAutoincSeq ()Ljava/lang/String; + public final fun getDelegate ()Lorg/jetbrains/exposed/sql/ColumnType; + public final fun getNextValExpression ()Lorg/jetbrains/exposed/sql/NextVal; + public fun getNullable ()Z + public fun hashCode ()I + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun setNullable (Z)V + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueToString (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/Avg : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;I)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/BasicBinaryColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/Between : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getFrom ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getTo ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract class org/jetbrains/exposed/sql/BiCompositeColumn : org/jetbrains/exposed/sql/CompositeColumn { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + protected final fun getColumn1 ()Lorg/jetbrains/exposed/sql/Column; + protected final fun getColumn2 ()Lorg/jetbrains/exposed/sql/Column; + public fun getRealColumns ()Ljava/util/List; + public fun getRealColumnsWithValues (Ljava/lang/Object;)Ljava/util/Map; + public final fun getTransformFromValue ()Lkotlin/jvm/functions/Function1; + public final fun getTransformToValue ()Lkotlin/jvm/functions/Function2; + public fun restoreValueFromParts (Ljava/util/Map;)Ljava/lang/Object; +} + +public class org/jetbrains/exposed/sql/BinaryColumnType : org/jetbrains/exposed/sql/BasicBinaryColumnType { + public fun (I)V + public fun equals (Ljava/lang/Object;)Z + public final fun getLength ()I + public fun hashCode ()I + public fun sqlType ()Ljava/lang/String; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V +} + +public final class org/jetbrains/exposed/sql/BlobColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob; + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob; +} + +public final class org/jetbrains/exposed/sql/BooleanColumnType : org/jetbrains/exposed/sql/ColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/BooleanColumnType$Companion; + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Boolean; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/BooleanColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/ByteColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Byte; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/Case { + public fun ()V + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Expression;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun When (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CaseWhen; + public final fun getValue ()Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/CaseWhen { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun Else (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public final fun When (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CaseWhen; + public final fun getCases ()Ljava/util/List; + public final fun getValue ()Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/CaseWhenElse : org/jetbrains/exposed/sql/ExpressionWithColumnType, org/jetbrains/exposed/sql/ComplexExpression { + public fun (Lorg/jetbrains/exposed/sql/CaseWhen;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getCaseWhen ()Lorg/jetbrains/exposed/sql/CaseWhen; + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getElseResult ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Cast : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/CharColumnType : org/jetbrains/exposed/sql/StringColumnType { + public fun ()V + public fun (ILjava/lang/String;)V + public synthetic fun (ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getColLength ()I + public fun hashCode ()I + public fun sqlType ()Ljava/lang/String; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V +} + +public final class org/jetbrains/exposed/sql/CharLength : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/CharacterColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Character; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/CheckConstraint : org/jetbrains/exposed/sql/DdlAware { + public static final field Companion Lorg/jetbrains/exposed/sql/CheckConstraint$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/CheckConstraint; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/CheckConstraint;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/CheckConstraint; + public fun createStatement ()Ljava/util/List; + public fun dropStatement ()Ljava/util/List; + public fun equals (Ljava/lang/Object;)Z + public final fun getCheckName ()Ljava/lang/String; + public final fun getCheckOp ()Ljava/lang/String; + public final fun getTableName ()Ljava/lang/String; + public fun hashCode ()I + public fun modifyStatement ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/CheckConstraint$Companion { +} + +public final class org/jetbrains/exposed/sql/Coalesce : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;[Lorg/jetbrains/exposed/sql/Expression;)V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Column : org/jetbrains/exposed/sql/ExpressionWithColumnType, java/lang/Comparable, org/jetbrains/exposed/sql/DdlAware { + public fun (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;)V + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo (Lorg/jetbrains/exposed/sql/Column;)I + public fun createStatement ()Ljava/util/List; + public final fun defaultValueInDb ()Lorg/jetbrains/exposed/sql/Expression; + public final fun descriptionDdl (Z)Ljava/lang/String; + public static synthetic fun descriptionDdl$default (Lorg/jetbrains/exposed/sql/Column;ZILjava/lang/Object;)Ljava/lang/String; + public fun dropStatement ()Ljava/util/List; + public fun equals (Ljava/lang/Object;)Z + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getDdl ()Ljava/util/List; + public final fun getDefaultValueFun ()Lkotlin/jvm/functions/Function0; + public final fun getForeignKey ()Lorg/jetbrains/exposed/sql/ForeignKeyConstraint; + public final fun getName ()Ljava/lang/String; + public final fun getReferee ()Lorg/jetbrains/exposed/sql/Column; + public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; + public fun hashCode ()I + public fun modifyStatement ()Ljava/util/List; + public final fun modifyStatements (Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; + public final fun nameInDatabaseCase ()Ljava/lang/String; + public final fun nameUnquoted ()Ljava/lang/String; + public final fun referee ()Lorg/jetbrains/exposed/sql/Column; + public final fun setDefaultValueFun (Lkotlin/jvm/functions/Function0;)V + public final fun setForeignKey (Lorg/jetbrains/exposed/sql/ForeignKeyConstraint;)V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun toString ()Ljava/lang/String; + public final fun withColumnType (Lorg/jetbrains/exposed/sql/IColumnType;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/ColumnDiff { + public static final field Companion Lorg/jetbrains/exposed/sql/ColumnDiff$Companion; + public fun (ZZZZ)V + public final fun component1 ()Z + public final fun component2 ()Z + public final fun component3 ()Z + public final fun component4 ()Z + public final fun copy (ZZZZ)Lorg/jetbrains/exposed/sql/ColumnDiff; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/ColumnDiff;ZZZZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/ColumnDiff; + public fun equals (Ljava/lang/Object;)Z + public final fun getAutoInc ()Z + public final fun getCaseSensitiveName ()Z + public final fun getDefaults ()Z + public final fun getNullability ()Z + public final fun hasDifferences ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/ColumnDiff$Companion { + public final fun getAllChanged ()Lorg/jetbrains/exposed/sql/ColumnDiff; + public final fun getNoneChanged ()Lorg/jetbrains/exposed/sql/ColumnDiff; +} + +public abstract class org/jetbrains/exposed/sql/ColumnSet : org/jetbrains/exposed/sql/FieldSet { + public fun ()V + public abstract fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public abstract fun describe (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public abstract fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public abstract fun getColumns ()Ljava/util/List; + public fun getFields ()Ljava/util/List; + public fun getRealFields ()Ljava/util/List; + public fun getSource ()Lorg/jetbrains/exposed/sql/ColumnSet; + public abstract fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public abstract fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun join$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public abstract fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public abstract fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun slice (Ljava/util/List;)Lorg/jetbrains/exposed/sql/FieldSet; + public final fun slice (Lorg/jetbrains/exposed/sql/Expression;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/FieldSet; +} + +public abstract class org/jetbrains/exposed/sql/ColumnType : org/jetbrains/exposed/sql/IColumnType { + public fun ()V + public fun (Z)V + public synthetic fun (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public fun getNullable ()Z + public fun hashCode ()I + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun setNullable (Z)V + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun toString ()Ljava/lang/String; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueToString (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/ColumnTypeKt { + public static final fun getAutoIncColumnType (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/AutoIncColumnType; + public static final fun isAutoInc (Lorg/jetbrains/exposed/sql/IColumnType;)Z +} + +public abstract class org/jetbrains/exposed/sql/ComparisonOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)V + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getOpSign ()Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract interface class org/jetbrains/exposed/sql/ComplexExpression { +} + +public abstract class org/jetbrains/exposed/sql/CompositeColumn : org/jetbrains/exposed/sql/Expression { + public fun ()V + public abstract fun getRealColumns ()Ljava/util/List; + public abstract fun getRealColumnsWithValues (Ljava/lang/Object;)Ljava/util/Map; + public abstract fun restoreValueFromParts (Ljava/util/Map;)Ljava/lang/Object; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/CompositeSqlLogger : org/jetbrains/exposed/sql/SqlLogger, org/jetbrains/exposed/sql/statements/StatementInterceptor { + public fun ()V + public final fun addLogger (Lorg/jetbrains/exposed/sql/SqlLogger;)V + public fun afterCommit (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun afterExecution (Lorg/jetbrains/exposed/sql/Transaction;Ljava/util/List;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public fun afterRollback (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun afterStatementPrepared (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public fun beforeCommit (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun beforeExecution (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/StatementContext;)V + public fun beforeRollback (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun keepUserDataInTransactionStoreOnCommit (Ljava/util/Map;)Ljava/util/Map; + public fun log (Lorg/jetbrains/exposed/sql/statements/StatementContext;Lorg/jetbrains/exposed/sql/Transaction;)V + public final fun removeLogger (Lorg/jetbrains/exposed/sql/SqlLogger;)V +} + +public abstract class org/jetbrains/exposed/sql/CompoundBooleanOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public synthetic fun (Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Concat : org/jetbrains/exposed/sql/Function { + public fun (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()[Lorg/jetbrains/exposed/sql/Expression; + public final fun getSeparator ()Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Count : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Expression;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDistinct ()Z + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/CumeDist : org/jetbrains/exposed/sql/WindowFunction { + public fun ()V + public fun (I)V + public synthetic fun (IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract interface class org/jetbrains/exposed/sql/CurrentOrFollowing : org/jetbrains/exposed/sql/WindowFrameBound { +} + +public abstract interface class org/jetbrains/exposed/sql/CurrentOrPreceding : org/jetbrains/exposed/sql/WindowFrameBound { +} + +public final class org/jetbrains/exposed/sql/CurrentRowWindowFrameBound : org/jetbrains/exposed/sql/CurrentOrFollowing, org/jetbrains/exposed/sql/CurrentOrPreceding, org/jetbrains/exposed/sql/WindowFrameBound { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/CurrentRowWindowFrameBound; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/CustomEnumerationColumnType : org/jetbrains/exposed/sql/StringColumnType { + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public final fun getFromDb ()Lkotlin/jvm/functions/Function1; + public final fun getName ()Ljava/lang/String; + public final fun getSql ()Ljava/lang/String; + public final fun getToDb ()Lkotlin/jvm/functions/Function1; + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Enum; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public class org/jetbrains/exposed/sql/CustomFunction : org/jetbrains/exposed/sql/Function { + public fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;[Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()[Lorg/jetbrains/exposed/sql/Expression; + public final fun getFunctionName ()Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/CustomOperator : org/jetbrains/exposed/sql/Function { + public fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getOperatorName ()Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Database { + public static final field Companion Lorg/jetbrains/exposed/sql/Database$Companion; + public synthetic fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun defaultFetchSize (I)Lorg/jetbrains/exposed/sql/Database; + public final fun getConfig ()Lorg/jetbrains/exposed/sql/DatabaseConfig; + public final fun getConnector ()Lkotlin/jvm/functions/Function0; + public final fun getDefaultFetchSize ()Ljava/lang/Integer; + public final fun getDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; + public final fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi; + public final fun getSupportsAlterTableWithAddColumn ()Z + public final fun getSupportsMultipleResultSets ()Z + public final fun getUrl ()Ljava/lang/String; + public final fun getUseNestedTransactions ()Z + public final fun getVendor ()Ljava/lang/String; + 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 { + public final fun connect (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Database; + public final fun connect (Ljavax/sql/DataSource;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Database; + public final fun connect (Lkotlin/jvm/functions/Function0;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Database; + public static synthetic fun connect$default (Lorg/jetbrains/exposed/sql/Database$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Database; + public static synthetic fun connect$default (Lorg/jetbrains/exposed/sql/Database$Companion;Ljavax/sql/DataSource;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Database; + public static synthetic fun connect$default (Lorg/jetbrains/exposed/sql/Database$Companion;Lkotlin/jvm/functions/Function0;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Database; + public final fun connectPool (Ljavax/sql/ConnectionPoolDataSource;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Database; + public static synthetic fun connectPool$default (Lorg/jetbrains/exposed/sql/Database$Companion;Ljavax/sql/ConnectionPoolDataSource;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/exposed/sql/DatabaseConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Database; + public final fun getDefaultIsolationLevel (Lorg/jetbrains/exposed/sql/Database;)I + public final fun getDialectName (Ljava/lang/String;)Ljava/lang/String; + public final fun registerDialect (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public final fun registerJdbcDriver (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +} + +public final class org/jetbrains/exposed/sql/DatabaseConfig { + public static final field Companion Lorg/jetbrains/exposed/sql/DatabaseConfig$Companion; + public synthetic fun (Lorg/jetbrains/exposed/sql/SqlLogger;ZLjava/lang/Integer;IIJJZLjava/lang/Long;IZLorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;IZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDefaultFetchSize ()Ljava/lang/Integer; + public final fun getDefaultIsolationLevel ()I + public final fun getDefaultMaxRepetitionDelay ()J + public final fun getDefaultMinRepetitionDelay ()J + public final fun getDefaultReadOnly ()Z + public final fun getDefaultRepetitionAttempts ()I + public final fun getDefaultSchema ()Lorg/jetbrains/exposed/sql/Schema; + public final fun getExplicitDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; + public final fun getKeepLoadedReferencesOutOfTransaction ()Z + public final fun getLogTooMuchResultSetsThreshold ()I + public final fun getMaxEntitiesToStoreInCachePerEntity ()I + public final fun getPreserveKeywordCasing ()Z + public final fun getSqlLogger ()Lorg/jetbrains/exposed/sql/SqlLogger; + public final fun getUseNestedTransactions ()Z + public final fun getWarnLongQueriesDuration ()Ljava/lang/Long; +} + +public final class org/jetbrains/exposed/sql/DatabaseConfig$Builder { + public fun ()V + public fun (Lorg/jetbrains/exposed/sql/SqlLogger;ZLjava/lang/Integer;IIJJZLjava/lang/Long;IZLorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;IZ)V + public synthetic fun (Lorg/jetbrains/exposed/sql/SqlLogger;ZLjava/lang/Integer;IIJJZLjava/lang/Long;IZLorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;IZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDefaultFetchSize ()Ljava/lang/Integer; + public final fun getDefaultIsolationLevel ()I + public final fun getDefaultMaxRepetitionDelay ()J + public final fun getDefaultMinRepetitionDelay ()J + public final fun getDefaultReadOnly ()Z + public final fun getDefaultRepetitionAttempts ()I + public final fun getDefaultSchema ()Lorg/jetbrains/exposed/sql/Schema; + public final fun getExplicitDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; + public final fun getKeepLoadedReferencesOutOfTransaction ()Z + public final fun getLogTooMuchResultSetsThreshold ()I + public final fun getMaxEntitiesToStoreInCachePerEntity ()I + public final fun getPreserveKeywordCasing ()Z + public final fun getSqlLogger ()Lorg/jetbrains/exposed/sql/SqlLogger; + public final fun getUseNestedTransactions ()Z + public final fun getWarnLongQueriesDuration ()Ljava/lang/Long; + public final fun setDefaultFetchSize (Ljava/lang/Integer;)V + public final fun setDefaultIsolationLevel (I)V + public final fun setDefaultMaxRepetitionDelay (J)V + public final fun setDefaultMinRepetitionDelay (J)V + public final fun setDefaultReadOnly (Z)V + public final fun setDefaultRepetitionAttempts (I)V + public final fun setDefaultSchema (Lorg/jetbrains/exposed/sql/Schema;)V + public final fun setExplicitDialect (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)V + public final fun setKeepLoadedReferencesOutOfTransaction (Z)V + public final fun setLogTooMuchResultSetsThreshold (I)V + public final fun setMaxEntitiesToStoreInCachePerEntity (I)V + public final fun setPreserveKeywordCasing (Z)V + public final fun setSqlLogger (Lorg/jetbrains/exposed/sql/SqlLogger;)V + public final fun setUseNestedTransactions (Z)V + public final fun setWarnLongQueriesDuration (Ljava/lang/Long;)V +} + +public final class org/jetbrains/exposed/sql/DatabaseConfig$Companion { + public final fun invoke (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/DatabaseConfig; + public static synthetic fun invoke$default (Lorg/jetbrains/exposed/sql/DatabaseConfig$Companion;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/DatabaseConfig; +} + +public abstract interface class org/jetbrains/exposed/sql/DatabaseConnectionAutoRegistration : kotlin/jvm/functions/Function1 { +} + +public final class org/jetbrains/exposed/sql/DatabaseKt { + public static final fun getName (Lorg/jetbrains/exposed/sql/Database;)Ljava/lang/String; +} + +public abstract interface class org/jetbrains/exposed/sql/DdlAware { + public abstract fun createStatement ()Ljava/util/List; + public abstract fun dropStatement ()Ljava/util/List; + public abstract fun modifyStatement ()Ljava/util/List; +} + +public final class org/jetbrains/exposed/sql/DecimalColumnType : org/jetbrains/exposed/sql/ColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/DecimalColumnType$Companion; + public fun (II)V + public fun equals (Ljava/lang/Object;)Z + public final fun getPrecision ()I + public final fun getScale ()I + public fun hashCode ()I + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/math/BigDecimal; +} + +public final class org/jetbrains/exposed/sql/DecimalColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/DenseRank : org/jetbrains/exposed/sql/WindowFunction { + public fun ()V + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/DivideOp : org/jetbrains/exposed/sql/CustomOperator { + public static final field Companion Lorg/jetbrains/exposed/sql/DivideOp$Companion; + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V +} + +public final class org/jetbrains/exposed/sql/DivideOp$Companion { + public final fun withScale (Lorg/jetbrains/exposed/sql/DivideOp;I)Lorg/jetbrains/exposed/sql/DivideOp; +} + +public final class org/jetbrains/exposed/sql/DoubleColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Double; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/EmptySizedIterable : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker, org/jetbrains/exposed/sql/SizedIterable { + public fun ()V + public fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun count ()J + public fun empty ()Z + public fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun hasNext ()Z + public fun iterator ()Ljava/util/Iterator; + public fun limit (IJ)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun next ()Ljava/lang/Object; + public fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun remove ()V +} + +public final class org/jetbrains/exposed/sql/EntityIDColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun (Lorg/jetbrains/exposed/sql/Column;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getIdColumn ()Lorg/jetbrains/exposed/sql/Column; + public fun hashCode ()I + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Lorg/jetbrains/exposed/dao/id/EntityID; +} + +public final class org/jetbrains/exposed/sql/EnumerationColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun (Lkotlin/reflect/KClass;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getKlass ()Lkotlin/reflect/KClass; + public fun hashCode ()I + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Integer; + public synthetic fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Enum; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/EnumerationNameColumnType : org/jetbrains/exposed/sql/VarCharColumnType { + public fun (Lkotlin/reflect/KClass;I)V + public fun equals (Ljava/lang/Object;)Z + public final fun getKlass ()Lkotlin/reflect/KClass; + public fun hashCode ()I + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Enum; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/EqOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/EqSubQueryOp : org/jetbrains/exposed/sql/SubQueryOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)V +} + +public final class org/jetbrains/exposed/sql/Except : org/jetbrains/exposed/sql/SetOperation { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public fun copy ()Lorg/jetbrains/exposed/sql/Intersect; + public synthetic fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun getOperationName ()Ljava/lang/String; + public synthetic fun withDistinct (Z)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun withDistinct (Z)Lorg/jetbrains/exposed/sql/SetOperation; +} + +public final class org/jetbrains/exposed/sql/Exists : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public final fun getQuery ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract interface annotation class org/jetbrains/exposed/sql/ExperimentalKeywordApi : java/lang/annotation/Annotation { +} + +public abstract class org/jetbrains/exposed/sql/Expression { + public static final field Companion Lorg/jetbrains/exposed/sql/Expression$Companion; + public fun ()V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public abstract fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/Expression$Companion { + public final fun build (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/ExpressionAlias : org/jetbrains/exposed/sql/Expression { + public fun (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)V + public final fun aliasOnlyExpression ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getAlias ()Ljava/lang/String; + public final fun getDelegate ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/ExpressionKt { + public static final fun append (Lorg/jetbrains/exposed/sql/QueryBuilder;[Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/QueryBuilder; + public static final fun appendTo (Ljava/lang/Iterable;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/QueryBuilder; + public static synthetic fun appendTo$default (Ljava/lang/Iterable;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/QueryBuilder; +} + +public abstract class org/jetbrains/exposed/sql/ExpressionWithColumnType : org/jetbrains/exposed/sql/Expression { + public fun ()V + public abstract fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; +} + +public abstract interface class org/jetbrains/exposed/sql/FieldSet { + public abstract fun getFields ()Ljava/util/List; + public abstract fun getRealFields ()Ljava/util/List; + public abstract fun getSource ()Lorg/jetbrains/exposed/sql/ColumnSet; +} + +public final class org/jetbrains/exposed/sql/FieldSet$DefaultImpls { + public static fun getRealFields (Lorg/jetbrains/exposed/sql/FieldSet;)Ljava/util/List; +} + +public final class org/jetbrains/exposed/sql/FirstValue : org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/FloatColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Float; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/ForeignKeyConstraint : org/jetbrains/exposed/sql/DdlAware { + public fun (Ljava/util/Map;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)V + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)V + public final fun component1 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ForeignKeyConstraint; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/ForeignKeyConstraint;Ljava/util/Map;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/ForeignKeyConstraint; + public fun createStatement ()Ljava/util/List; + public fun dropStatement ()Ljava/util/List; + public fun equals (Ljava/lang/Object;)Z + public final fun getCustomFkName ()Ljava/lang/String; + public final fun getDeleteRule ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public final fun getFkName ()Ljava/lang/String; + public final fun getFrom ()Ljava/util/LinkedHashSet; + public final fun getFromTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun getFromTableName ()Ljava/lang/String; + public final fun getReferences ()Ljava/util/Map; + public final fun getTarget ()Ljava/util/LinkedHashSet; + public final fun getTargetTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun getTargetTableName ()Ljava/lang/String; + public final fun getUpdateRule ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun hashCode ()I + public fun modifyStatement ()Ljava/util/List; + public final fun plus (Lorg/jetbrains/exposed/sql/ForeignKeyConstraint;)Lorg/jetbrains/exposed/sql/ForeignKeyConstraint; + public final fun targetOf (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public fun toString ()Ljava/lang/String; +} + +public abstract class org/jetbrains/exposed/sql/Function : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/IColumnType;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; +} + +public final class org/jetbrains/exposed/sql/GreaterEqOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/GreaterOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/GroupConcat : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Z[Lkotlin/Pair;)V + public final fun getDistinct ()Z + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getOrderBy ()[Lkotlin/Pair; + public final fun getSeparator ()Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract interface class org/jetbrains/exposed/sql/IColumnType { + public abstract fun getNullable ()Z + public abstract fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public abstract fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public abstract fun setNullable (Z)V + public abstract fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public abstract fun sqlType ()Ljava/lang/String; + public abstract fun validateValueBeforeUpdate (Ljava/lang/Object;)V + public abstract fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun valueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun valueToString (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/IColumnType$DefaultImpls { + public static fun nonNullValueToString (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)Ljava/lang/String; + public static fun notNullValueToDB (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)Ljava/lang/Object; + public static fun readObject (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/sql/ResultSet;I)Ljava/lang/Object; + public static fun setParameter (Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public static fun validateValueBeforeUpdate (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)V + public static fun valueFromDB (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)Ljava/lang/Object; + public static fun valueToDB (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)Ljava/lang/Object; + public static fun valueToString (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)Ljava/lang/String; +} + +public abstract interface class org/jetbrains/exposed/sql/IDateColumnType { + public abstract fun getHasTimePart ()Z +} + +public abstract interface class org/jetbrains/exposed/sql/ISqlExpressionBuilder { + public abstract fun asLiteral (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/LiteralOp; + public abstract fun between (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Between; + public abstract fun bitwiseAnd (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/AndBitOp; + public abstract fun bitwiseAnd (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/AndBitOp; + public abstract fun bitwiseOr (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/OrBitOp; + public abstract fun bitwiseOr (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/OrBitOp; + public abstract fun bitwiseXor (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/XorBitOp; + public abstract fun bitwiseXor (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/XorBitOp; + public abstract fun case (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Case; + public abstract fun coalesce (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Coalesce; + public abstract fun concat (Ljava/lang/String;Ljava/util/List;)Lorg/jetbrains/exposed/sql/Concat; + public abstract fun concat ([Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Concat; + public abstract fun cumeDist ()Lorg/jetbrains/exposed/sql/CumeDist; + public abstract fun denseRank ()Lorg/jetbrains/exposed/sql/DenseRank; + public abstract fun div (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/DivideOp; + public abstract fun div (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/DivideOp; + public abstract fun eq (Lorg/jetbrains/exposed/sql/CompositeColumn;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun eq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun eq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun eq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun eqSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/EqSubQueryOp; + public abstract fun firstValue (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/FirstValue; + public abstract fun greater (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/GreaterOp; + public abstract fun greater (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterOp; + public abstract fun greaterEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterOp; + public abstract fun greaterEq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public abstract fun greaterEq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public abstract fun greaterEqEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public abstract fun hasFlag (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/EqOp; + public abstract fun hasFlag (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/EqOp; + public abstract fun inList (Lkotlin/Pair;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun inList (Lkotlin/Triple;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun inList (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun inListIds (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun inSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/InSubQueryOp; + public abstract fun intToDecimal (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/NoOpConversion; + public abstract fun isDistinctFrom (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public abstract fun isDistinctFrom (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public abstract fun isDistinctFromEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public abstract fun isNotDistinctFrom (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public abstract fun isNotDistinctFrom (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public abstract fun isNotDistinctFromEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public abstract fun isNotNull (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNotNullOp; + public abstract fun isNull (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNullOp; + public abstract fun lag (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Lag; + public abstract fun lastValue (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LastValue; + public abstract fun lead (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Lead; + public abstract fun less (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LessOp; + public abstract fun less (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessOp; + public abstract fun lessEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessOp; + public abstract fun lessEq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LessEqOp; + public abstract fun lessEq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessEqOp; + public abstract fun lessEqEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessEqOp; + public abstract fun like (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun like (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun like (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun likeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun likeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun likeWithEntityIDAndExpression (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun match (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun match (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/FunctionProvider$MatchMode;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun minus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/MinusOp; + public abstract fun minus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/MinusOp; + public abstract fun mod (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ModOp; + public abstract fun mod (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ModOp; + public abstract fun modWithEntityId (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun modWithEntityId2 (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun modWithEntityId3 (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun neq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun neq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun neq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public abstract fun notEqSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotEqSubQueryOp; + public abstract fun notInList (Lkotlin/Pair;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun notInList (Lkotlin/Triple;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun notInList (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun notInListIds (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public abstract fun notInSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotInSubQueryOp; + public abstract fun notLike (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun notLike (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun notLike (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun notLikeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun notLikeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun notLikeWithEntityIDAndExpression (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public abstract fun nthValue (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/NthValue; + public abstract fun ntile (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Ntile; + public abstract fun percentRank ()Lorg/jetbrains/exposed/sql/PercentRank; + public abstract fun plus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/PlusOp; + public abstract fun plus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/PlusOp; + public abstract fun rank ()Lorg/jetbrains/exposed/sql/Rank; + public abstract fun regexp (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/RegexpOp; + public abstract fun regexp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Z)Lorg/jetbrains/exposed/sql/RegexpOp; + public abstract fun rem (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ModOp; + public abstract fun rem (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ModOp; + public abstract fun remWithEntityId (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun remWithEntityId2 (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun remWithEntityId3 (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public abstract fun rowNumber ()Lorg/jetbrains/exposed/sql/RowNumber; + public abstract fun times (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/TimesOp; + public abstract fun times (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/TimesOp; + public abstract fun wrap (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/QueryParameter; +} + +public final class org/jetbrains/exposed/sql/ISqlExpressionBuilder$DefaultImpls { + public static fun asLiteral (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static fun between (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Between; + public static fun bitwiseAnd (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/AndBitOp; + public static fun bitwiseAnd (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/AndBitOp; + public static fun bitwiseOr (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/OrBitOp; + public static fun bitwiseOr (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/OrBitOp; + public static fun bitwiseXor (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/XorBitOp; + public static fun bitwiseXor (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/XorBitOp; + public static fun case (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Case; + public static synthetic fun case$default (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Case; + public static fun coalesce (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Coalesce; + public static fun concat (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Ljava/lang/String;Ljava/util/List;)Lorg/jetbrains/exposed/sql/Concat; + public static fun concat (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Concat; + public static synthetic fun concat$default (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Concat; + public static fun cumeDist (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;)Lorg/jetbrains/exposed/sql/CumeDist; + public static fun denseRank (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;)Lorg/jetbrains/exposed/sql/DenseRank; + public static fun div (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/DivideOp; + public static fun div (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/DivideOp; + public static fun eq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/CompositeColumn;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public static fun eq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static fun eq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/Op; + public static fun eq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public static fun eqSubQuery (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/EqSubQueryOp; + public static fun firstValue (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/FirstValue; + public static fun greater (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/GreaterOp; + public static fun greater (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterOp; + public static fun greaterEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterOp; + public static fun greaterEq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public static fun greaterEq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public static fun greaterEqEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public static fun hasFlag (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/EqOp; + public static fun hasFlag (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/EqOp; + public static fun inList (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lkotlin/Pair;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun inList (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lkotlin/Triple;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun inList (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun inListIds (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun inSubQuery (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/InSubQueryOp; + public static fun intToDecimal (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/NoOpConversion; + public static fun isDistinctFrom (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public static fun isDistinctFrom (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public static fun isDistinctFromEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public static fun isNotDistinctFrom (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public static fun isNotDistinctFrom (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public static fun isNotDistinctFromEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public static fun isNotNull (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNotNullOp; + public static fun isNull (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNullOp; + public static fun lag (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Lag; + public static synthetic fun lag$default (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Lag; + public static fun lastValue (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LastValue; + public static fun lead (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Lead; + public static synthetic fun lead$default (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Lead; + public static fun less (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LessOp; + public static fun less (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessOp; + public static fun lessEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessOp; + public static fun lessEq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LessEqOp; + public static fun lessEq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessEqOp; + public static fun lessEqEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessEqOp; + public static fun like (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun like (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun like (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun likeWithEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun likeWithEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun likeWithEntityIDAndExpression (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun match (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Op; + public static fun match (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/FunctionProvider$MatchMode;)Lorg/jetbrains/exposed/sql/Op; + public static fun minus (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/MinusOp; + public static fun minus (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/MinusOp; + public static fun mod (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ModOp; + public static fun mod (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ModOp; + public static fun modWithEntityId (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun modWithEntityId2 (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun modWithEntityId3 (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun neq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static fun neq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/Op; + public static fun neq (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public static fun notEqSubQuery (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotEqSubQueryOp; + public static fun notInList (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lkotlin/Pair;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun notInList (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lkotlin/Triple;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun notInList (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun notInListIds (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public static fun notInSubQuery (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotInSubQueryOp; + public static fun notLike (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun notLike (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun notLike (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun notLikeWithEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun notLikeWithEntityID (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun notLikeWithEntityIDAndExpression (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public static fun nthValue (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/NthValue; + public static fun ntile (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Ntile; + public static fun percentRank (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;)Lorg/jetbrains/exposed/sql/PercentRank; + public static fun plus (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/PlusOp; + public static fun plus (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/PlusOp; + public static fun rank (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;)Lorg/jetbrains/exposed/sql/Rank; + public static fun regexp (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/RegexpOp; + public static fun regexp (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Z)Lorg/jetbrains/exposed/sql/RegexpOp; + public static synthetic fun regexp$default (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/RegexpOp; + public static fun rem (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ModOp; + public static fun rem (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ModOp; + public static fun remWithEntityId (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun remWithEntityId2 (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun remWithEntityId3 (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static fun rowNumber (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;)Lorg/jetbrains/exposed/sql/RowNumber; + public static fun times (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/TimesOp; + public static fun times (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/TimesOp; + public static fun wrap (Lorg/jetbrains/exposed/sql/ISqlExpressionBuilder;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/QueryParameter; +} + +public final class org/jetbrains/exposed/sql/InSubQueryOp : org/jetbrains/exposed/sql/SubQueryOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)V +} + +public final class org/jetbrains/exposed/sql/Index : org/jetbrains/exposed/sql/DdlAware { + public fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Z + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lorg/jetbrains/exposed/sql/Op; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Lorg/jetbrains/exposed/sql/Table; + public final fun copy (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;)Lorg/jetbrains/exposed/sql/Index; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/Index;Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Index; + public fun createStatement ()Ljava/util/List; + public fun dropStatement ()Ljava/util/List; + public fun equals (Ljava/lang/Object;)Z + public final fun getColumns ()Ljava/util/List; + public final fun getCustomName ()Ljava/lang/String; + public final fun getFilterCondition ()Lorg/jetbrains/exposed/sql/Op; + public final fun getFunctions ()Ljava/util/List; + public final fun getFunctionsTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun getIndexName ()Ljava/lang/String; + public final fun getIndexType ()Ljava/lang/String; + public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun getUnique ()Z + public fun hashCode ()I + public fun modifyStatement ()Ljava/util/List; + public final fun onlyNameDiffer (Lorg/jetbrains/exposed/sql/Index;)Z + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/IntegerColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Integer; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/Intersect : org/jetbrains/exposed/sql/SetOperation { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public fun copy ()Lorg/jetbrains/exposed/sql/Intersect; + public synthetic fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public synthetic fun withDistinct (Z)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun withDistinct (Z)Lorg/jetbrains/exposed/sql/SetOperation; +} + +public final class org/jetbrains/exposed/sql/IsDistinctFromOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpression1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpression2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/IsNotDistinctFromOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpression1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpression2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/IsNotNullOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/IsNullOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/IterableExKt { + public static final fun emptySized ()Lorg/jetbrains/exposed/sql/SizedIterable; + public static final fun mapLazy (Lorg/jetbrains/exposed/sql/SizedIterable;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/sql/Join : org/jetbrains/exposed/sql/ColumnSet { + public fun (Lorg/jetbrains/exposed/sql/ColumnSet;)V + public fun (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun alreadyInJoin (Lorg/jetbrains/exposed/sql/Table;)Z + public fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun describe (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun getColumns ()Ljava/util/List; + public final fun getTable ()Lorg/jetbrains/exposed/sql/ColumnSet; + public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; +} + +public final class org/jetbrains/exposed/sql/JoinType : java/lang/Enum { + public static final field CROSS Lorg/jetbrains/exposed/sql/JoinType; + public static final field FULL Lorg/jetbrains/exposed/sql/JoinType; + public static final field INNER Lorg/jetbrains/exposed/sql/JoinType; + public static final field LEFT Lorg/jetbrains/exposed/sql/JoinType; + public static final field RIGHT Lorg/jetbrains/exposed/sql/JoinType; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/JoinType; + public static fun values ()[Lorg/jetbrains/exposed/sql/JoinType; +} + +public abstract interface class org/jetbrains/exposed/sql/JsonColumnMarker { + public abstract fun getUsesBinaryFormat ()Z +} + +public final class org/jetbrains/exposed/sql/Key { + public fun ()V +} + +public final class org/jetbrains/exposed/sql/Lag : org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDefaultValue ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public final fun getExpr ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public final fun getOffset ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/LargeTextColumnType : org/jetbrains/exposed/sql/TextColumnType { + public fun ()V + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun preciseType ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/LastValue : org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/LazySizedCollection : org/jetbrains/exposed/sql/SizedIterable { + public fun (Lorg/jetbrains/exposed/sql/SizedIterable;)V + public fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun count ()J + public fun empty ()Z + public fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun getWrapper ()Ljava/util/List; + public final fun isLoaded ()Z + public fun iterator ()Ljava/util/Iterator; + public fun limit (IJ)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/sql/Lead : org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDefaultValue ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public final fun getExpr ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public final fun getOffset ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/LessEqOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/LessOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/LikeEscapeOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLjava/lang/Character;)V + public final fun getEscapeChar ()Ljava/lang/Character; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/LikeOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/LikePattern { + public static final field Companion Lorg/jetbrains/exposed/sql/LikePattern$Companion; + public fun (Ljava/lang/String;Ljava/lang/Character;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Character;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Character; + public final fun copy (Ljava/lang/String;Ljava/lang/Character;)Lorg/jetbrains/exposed/sql/LikePattern; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/LikePattern;Ljava/lang/String;Ljava/lang/Character;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/LikePattern; + public fun equals (Ljava/lang/Object;)Z + public final fun getEscapeChar ()Ljava/lang/Character; + public final fun getPattern ()Ljava/lang/String; + public fun hashCode ()I + public final fun plus (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikePattern; + public final fun plus (Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikePattern; + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/LikePattern$Companion { + public final fun ofLiteral (Ljava/lang/String;C)Lorg/jetbrains/exposed/sql/LikePattern; + public static synthetic fun ofLiteral$default (Lorg/jetbrains/exposed/sql/LikePattern$Companion;Ljava/lang/String;CILjava/lang/Object;)Lorg/jetbrains/exposed/sql/LikePattern; +} + +public final class org/jetbrains/exposed/sql/LiteralOp : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getValue ()Ljava/lang/Object; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Locate : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getSubstring ()Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/LongColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Long; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/LowerCase : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Max : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/MediumTextColumnType : org/jetbrains/exposed/sql/TextColumnType { + public fun ()V + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun preciseType ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/Min : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/MinusOp : org/jetbrains/exposed/sql/CustomOperator { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V +} + +public final class org/jetbrains/exposed/sql/ModOp : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/ModOp$Companion; + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/ModOp$Companion { +} + +public final class org/jetbrains/exposed/sql/NeqOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public abstract class org/jetbrains/exposed/sql/NextVal : org/jetbrains/exposed/sql/Function { + public synthetic fun (Lorg/jetbrains/exposed/sql/Sequence;Lorg/jetbrains/exposed/sql/IColumnType;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getSeq ()Lorg/jetbrains/exposed/sql/Sequence; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/NextVal$IntNextVal : org/jetbrains/exposed/sql/NextVal { + public fun (Lorg/jetbrains/exposed/sql/Sequence;)V +} + +public final class org/jetbrains/exposed/sql/NextVal$LongNextVal : org/jetbrains/exposed/sql/NextVal { + public fun (Lorg/jetbrains/exposed/sql/Sequence;)V +} + +public final class org/jetbrains/exposed/sql/NoOpConversion : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/NotEqSubQueryOp : org/jetbrains/exposed/sql/SubQueryOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)V +} + +public final class org/jetbrains/exposed/sql/NotExists : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public final fun getQuery ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/NotInSubQueryOp : org/jetbrains/exposed/sql/SubQueryOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)V +} + +public final class org/jetbrains/exposed/sql/NotLikeOp : org/jetbrains/exposed/sql/ComparisonOp { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/NotOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/NthValue : org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public final fun getN ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Ntile : org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V + public final fun getNumBuckets ()Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/OffsetFollowingWindowFrameBound : org/jetbrains/exposed/sql/OffsetWindowFrameBound, org/jetbrains/exposed/sql/CurrentOrFollowing { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V +} + +public final class org/jetbrains/exposed/sql/OffsetPrecedingWindowFrameBound : org/jetbrains/exposed/sql/OffsetWindowFrameBound, org/jetbrains/exposed/sql/CurrentOrPreceding { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V +} + +public class org/jetbrains/exposed/sql/OffsetWindowFrameBound : org/jetbrains/exposed/sql/WindowFrameBound { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/WindowFrameBoundDirection;)V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract class org/jetbrains/exposed/sql/Op : org/jetbrains/exposed/sql/Expression { + public static final field Companion Lorg/jetbrains/exposed/sql/Op$Companion; + public fun ()V +} + +public final class org/jetbrains/exposed/sql/Op$Companion { + public final fun build (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public final fun nullOp ()Lorg/jetbrains/exposed/sql/Op; +} + +public final class org/jetbrains/exposed/sql/Op$FALSE : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/Op$OpBoolean { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/Op$FALSE; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Op$TRUE : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/Op$OpBoolean { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/Op$TRUE; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/OpKt { + public static final fun and (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public static final fun and (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static final fun andIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public static final fun andIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static final fun andNot (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public static final fun blobParam (Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun booleanLiteral (Z)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun booleanParam (Z)Lorg/jetbrains/exposed/sql/Expression; + public static final fun byteLiteral (B)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun byteParam (B)Lorg/jetbrains/exposed/sql/Expression; + public static final fun compoundAnd (Ljava/util/List;)Lorg/jetbrains/exposed/sql/Op; + public static final fun compoundOr (Ljava/util/List;)Lorg/jetbrains/exposed/sql/Op; + public static final fun decimalLiteral (Ljava/math/BigDecimal;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun decimalParam (Ljava/math/BigDecimal;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun doubleLiteral (D)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun doubleParam (D)Lorg/jetbrains/exposed/sql/Expression; + public static final fun exists (Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/Exists; + public static final fun floatLiteral (F)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun floatParam (F)Lorg/jetbrains/exposed/sql/Expression; + public static final fun idParam (Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun intLiteral (I)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun intParam (I)Lorg/jetbrains/exposed/sql/Expression; + public static final fun longLiteral (J)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun longParam (J)Lorg/jetbrains/exposed/sql/Expression; + public static final fun not (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static final fun notExists (Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotExists; + public static final fun or (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public static final fun or (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static final fun orIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public static final fun orIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public static final fun orNot (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op; + public static final fun shortLiteral (S)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun shortParam (S)Lorg/jetbrains/exposed/sql/Expression; + public static final fun stringLiteral (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun stringParam (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun ubyteLiteral-7apg3OU (B)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun ubyteParam-7apg3OU (B)Lorg/jetbrains/exposed/sql/Expression; + public static final fun uintLiteral-WZ4Q5Ns (I)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun uintParam-WZ4Q5Ns (I)Lorg/jetbrains/exposed/sql/Expression; + public static final fun ulongLiteral-VKZWuLQ (J)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun ulongParam-VKZWuLQ (J)Lorg/jetbrains/exposed/sql/Expression; + public static final fun ushortLiteral-xj2QHRw (S)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun ushortParam-xj2QHRw (S)Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/OrBitOp : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/OrOp : org/jetbrains/exposed/sql/CompoundBooleanOp { + public fun (Ljava/util/List;)V +} + +public final class org/jetbrains/exposed/sql/PercentRank : org/jetbrains/exposed/sql/WindowFunction { + public fun ()V + public fun (I)V + public synthetic fun (IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/PlusOp : org/jetbrains/exposed/sql/CustomOperator { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V +} + +public final class org/jetbrains/exposed/sql/QueriesKt { + public static final fun batchInsert (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;ZZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun batchInsert (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;ZZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static synthetic fun batchInsert$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static synthetic fun batchInsert$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static final fun batchReplace (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun batchReplace (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static synthetic fun batchReplace$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static synthetic fun batchReplace$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun batchUpsert (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;)Ljava/util/List; + public static synthetic fun batchUpsert$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Iterable;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static synthetic fun batchUpsert$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/sequences/Sequence;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/List; + public static final fun deleteAll (Lorg/jetbrains/exposed/sql/Table;)I + public static final fun deleteIgnoreWhere (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Integer;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;)I + public static synthetic fun deleteIgnoreWhere$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Integer;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)I + public static final fun deleteWhere (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Integer;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;)I + public static synthetic fun deleteWhere$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Integer;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)I + public static final fun exists (Lorg/jetbrains/exposed/sql/Table;)Z + public static final fun insert (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/InsertStatement; + public static final fun insert (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/util/List;)Ljava/lang/Integer; + public static synthetic fun insert$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/util/List;ILjava/lang/Object;)Ljava/lang/Integer; + public static final fun insertAndGetId (Lorg/jetbrains/exposed/dao/id/IdTable;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/dao/id/EntityID; + public static final fun insertIgnore (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/InsertStatement; + public static final fun insertIgnore (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/util/List;)Ljava/lang/Integer; + public static synthetic fun insertIgnore$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/util/List;ILjava/lang/Object;)Ljava/lang/Integer; + public static final fun insertIgnoreAndGetId (Lorg/jetbrains/exposed/dao/id/IdTable;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/dao/id/EntityID; + public static final fun replace (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/ReplaceStatement; + public static final fun select (Lorg/jetbrains/exposed/sql/FieldSet;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public static final fun select (Lorg/jetbrains/exposed/sql/FieldSet;Lorg/jetbrains/exposed/sql/Op;)Lorg/jetbrains/exposed/sql/Query; + public static final fun selectAll (Lorg/jetbrains/exposed/sql/FieldSet;)Lorg/jetbrains/exposed/sql/Query; + public static final fun selectAllBatched (Lorg/jetbrains/exposed/sql/FieldSet;I)Ljava/lang/Iterable; + public static synthetic fun selectAllBatched$default (Lorg/jetbrains/exposed/sql/FieldSet;IILjava/lang/Object;)Ljava/lang/Iterable; + public static final fun selectBatched (Lorg/jetbrains/exposed/sql/FieldSet;ILkotlin/jvm/functions/Function1;)Ljava/lang/Iterable; + public static synthetic fun selectBatched$default (Lorg/jetbrains/exposed/sql/FieldSet;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Iterable; + public static final fun update (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I + public static final fun update (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;)I + public static synthetic fun update$default (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static synthetic fun update$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)I + public static final fun upsert (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; + public static synthetic fun upsert$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; +} + +public class org/jetbrains/exposed/sql/Query : org/jetbrains/exposed/sql/AbstractQuery { + public fun (Lorg/jetbrains/exposed/sql/FieldSet;Lorg/jetbrains/exposed/sql/Op;)V + public final fun adjustColumnSet (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public final fun adjustHaving (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public final fun adjustSlice (Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/Query; + public final fun adjustWhere (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public fun copy ()Lorg/jetbrains/exposed/sql/Query; + public synthetic fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun count ()J + public fun empty ()Z + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/sql/ResultSet; + public fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/Query; + public synthetic fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun getDistinct ()Z + public final fun getGroupedByColumns ()Ljava/util/List; + public final fun getHaving ()Lorg/jetbrains/exposed/sql/Op; + protected fun getQueryToExecute ()Lorg/jetbrains/exposed/sql/statements/Statement; + public fun getSet ()Lorg/jetbrains/exposed/sql/FieldSet; + public final fun getWhere ()Lorg/jetbrains/exposed/sql/Op; + public final fun groupBy ([Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Query; + public final fun hasCustomForUpdateState ()Z + public final fun having (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public final fun isForUpdate ()Z + public fun notForUpdate ()Lorg/jetbrains/exposed/sql/Query; + public synthetic fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/QueryBuilder;)Ljava/lang/String; + protected final fun setDistinct (Z)V + public fun setSet (Lorg/jetbrains/exposed/sql/FieldSet;)V + public synthetic fun withDistinct (Z)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun withDistinct (Z)Lorg/jetbrains/exposed/sql/Query; +} + +public final class org/jetbrains/exposed/sql/QueryAlias : org/jetbrains/exposed/sql/ColumnSet { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/lang/String;)V + public fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun describe (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun get (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun get (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Expression; + public final fun getAlias ()Ljava/lang/String; + public fun getColumns ()Ljava/util/List; + public fun getFields ()Ljava/util/List; + public final fun getQuery ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; +} + +public final class org/jetbrains/exposed/sql/QueryBuilder { + public fun (Z)V + public final fun append (C)Lorg/jetbrains/exposed/sql/QueryBuilder; + public final fun append (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/QueryBuilder; + public final fun append (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/QueryBuilder; + public final fun appendTo (Ljava/lang/Iterable;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function2;)V + public final fun appendTo ([Ljava/lang/Object;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun appendTo$default (Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/Iterable;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun appendTo$default (Lorg/jetbrains/exposed/sql/QueryBuilder;[Ljava/lang/Object;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun getArgs ()Ljava/util/List; + public final fun getPrepared ()Z + public final fun invoke (Lkotlin/jvm/functions/Function1;)V + public final fun registerArgument (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)V + public final fun registerArgument (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Object;)V + public final fun registerArguments (Lorg/jetbrains/exposed/sql/IColumnType;Ljava/lang/Iterable;)V + public fun toString ()Ljava/lang/String; + public final fun unaryPlus (C)Lorg/jetbrains/exposed/sql/QueryBuilder; + public final fun unaryPlus (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/QueryBuilder; + public final fun unaryPlus (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/QueryBuilder; +} + +public final class org/jetbrains/exposed/sql/QueryKt { + public static final fun andHaving (Lorg/jetbrains/exposed/sql/Query;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public static final fun andWhere (Lorg/jetbrains/exposed/sql/Query;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public static final fun orHaving (Lorg/jetbrains/exposed/sql/Query;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; + public static final fun orWhere (Lorg/jetbrains/exposed/sql/Query;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; +} + +public final class org/jetbrains/exposed/sql/QueryParameter : org/jetbrains/exposed/sql/Expression { + public fun (Ljava/lang/Object;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getSqlType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getValue ()Ljava/lang/Object; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Random : org/jetbrains/exposed/sql/Function { + public fun ()V + public fun (Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getSeed ()Ljava/lang/Integer; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Rank : org/jetbrains/exposed/sql/WindowFunction { + public fun ()V + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/ReferenceOption : java/lang/Enum { + public static final field CASCADE Lorg/jetbrains/exposed/sql/ReferenceOption; + public static final field NO_ACTION Lorg/jetbrains/exposed/sql/ReferenceOption; + public static final field RESTRICT Lorg/jetbrains/exposed/sql/ReferenceOption; + public static final field SET_DEFAULT Lorg/jetbrains/exposed/sql/ReferenceOption; + public static final field SET_NULL Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun toString ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ReferenceOption; + public static fun values ()[Lorg/jetbrains/exposed/sql/ReferenceOption; +} + +public final class org/jetbrains/exposed/sql/RegexpOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Z)V + public final fun getCaseSensitive ()Z + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/ResultRow { + public static final field Companion Lorg/jetbrains/exposed/sql/ResultRow$Companion; + public fun (Ljava/util/Map;[Ljava/lang/Object;)V + public synthetic fun (Ljava/util/Map;[Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun get (Lorg/jetbrains/exposed/sql/Expression;)Ljava/lang/Object; + public final fun getFieldIndex ()Ljava/util/Map; + public final fun getOrNull (Lorg/jetbrains/exposed/sql/Expression;)Ljava/lang/Object; + public final fun hasValue (Lorg/jetbrains/exposed/sql/Expression;)Z + public final fun set (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/Object;)V + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/ResultRow$Companion { + public final fun create (Ljava/sql/ResultSet;Ljava/util/Map;)Lorg/jetbrains/exposed/sql/ResultRow; + public final fun createAndFillDefaults (Ljava/util/List;)Lorg/jetbrains/exposed/sql/ResultRow; + public final fun createAndFillValues (Ljava/util/Map;)Lorg/jetbrains/exposed/sql/ResultRow; +} + +public final class org/jetbrains/exposed/sql/RowNumber : org/jetbrains/exposed/sql/WindowFunction { + public fun ()V + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/SQLExpressionBuilderKt { + public static final fun CustomLongFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomStringFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun avg (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/Avg; + public static synthetic fun avg$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Avg; + public static final fun castTo (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public static final fun charLength (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CharLength; + public static final fun count (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Count; + public static final fun countDistinct (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Count; + public static final fun function (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun groupConcat (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;ZLkotlin/Pair;)Lorg/jetbrains/exposed/sql/GroupConcat; + public static final fun groupConcat (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Z[Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/GroupConcat; + public static synthetic fun groupConcat$default (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;ZLkotlin/Pair;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/GroupConcat; + public static synthetic fun groupConcat$default (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Z[Lkotlin/Pair;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/GroupConcat; + public static final fun locate (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Locate; + public static final fun lowerCase (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LowerCase; + public static final fun max (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Max; + public static final fun min (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Min; + public static final fun nextIntVal (Lorg/jetbrains/exposed/sql/Sequence;)Lorg/jetbrains/exposed/sql/NextVal; + public static final fun nextLongVal (Lorg/jetbrains/exposed/sql/Sequence;)Lorg/jetbrains/exposed/sql/NextVal; + public static final fun stdDevPop (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/StdDevPop; + public static synthetic fun stdDevPop$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/StdDevPop; + public static final fun stdDevSamp (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/StdDevSamp; + public static synthetic fun stdDevSamp$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/StdDevSamp; + public static final fun substring (Lorg/jetbrains/exposed/sql/Expression;II)Lorg/jetbrains/exposed/sql/Substring; + public static final fun sum (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Sum; + public static final fun trim (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Trim; + public static final fun upperCase (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/UpperCase; + public static final fun varPop (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/VarPop; + public static synthetic fun varPop$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/VarPop; + public static final fun varSamp (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)Lorg/jetbrains/exposed/sql/VarSamp; + public static synthetic fun varSamp$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;IILjava/lang/Object;)Lorg/jetbrains/exposed/sql/VarSamp; +} + +public final class org/jetbrains/exposed/sql/SQLLogKt { + public static final fun addLogger (Lorg/jetbrains/exposed/sql/Transaction;[Lorg/jetbrains/exposed/sql/SqlLogger;)Lorg/jetbrains/exposed/sql/CompositeSqlLogger; + public static final fun getExposedLogger ()Lorg/slf4j/Logger; +} + +public final class org/jetbrains/exposed/sql/Schema { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Schema; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/Schema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Schema; + public final fun createStatement ()Ljava/util/List; + public final fun dropStatement (Z)Ljava/util/List; + public fun equals (Ljava/lang/Object;)Z + public final fun exists ()Z + public final fun getAuthorization ()Ljava/lang/String; + public final fun getDdl ()Ljava/util/List; + public final fun getDefaultTablespace ()Ljava/lang/String; + public final fun getIdentifier ()Ljava/lang/String; + public final fun getOn ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; + public final fun getQuota ()Ljava/lang/String; + public final fun getTemporaryTablespace ()Ljava/lang/String; + public fun hashCode ()I + public final fun setSchemaStatement ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/SchemaUtils { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/SchemaUtils; + public final fun addMissingColumnsStatements ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List; + public static synthetic fun addMissingColumnsStatements$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List; + public final fun checkCycle ([Lorg/jetbrains/exposed/sql/Table;)Z + public final fun checkExcessiveIndices ([Lorg/jetbrains/exposed/sql/Table;)V + public final fun checkMappingConsistence ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List; + public static synthetic fun checkMappingConsistence$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List; + public final fun create ([Lorg/jetbrains/exposed/sql/Table;Z)V + public static synthetic fun create$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)V + public final fun createDatabase ([Ljava/lang/String;Z)V + public static synthetic fun createDatabase$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Ljava/lang/String;ZILjava/lang/Object;)V + public final fun createFKey (Lorg/jetbrains/exposed/sql/ForeignKeyConstraint;)Ljava/util/List; + public final fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/util/List; + public final fun createMissingTablesAndColumns ([Lorg/jetbrains/exposed/sql/Table;ZZ)V + public static synthetic fun createMissingTablesAndColumns$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZZILjava/lang/Object;)V + public final fun createSchema ([Lorg/jetbrains/exposed/sql/Schema;Z)V + public static synthetic fun createSchema$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Schema;ZILjava/lang/Object;)V + public final fun createSequence ([Lorg/jetbrains/exposed/sql/Sequence;Z)V + public static synthetic fun createSequence$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Sequence;ZILjava/lang/Object;)V + public final fun createStatements ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/List; + public final fun drop ([Lorg/jetbrains/exposed/sql/Table;Z)V + public static synthetic fun drop$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)V + public final fun dropDatabase ([Ljava/lang/String;Z)V + public static synthetic fun dropDatabase$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Ljava/lang/String;ZILjava/lang/Object;)V + public final fun dropSchema ([Lorg/jetbrains/exposed/sql/Schema;ZZ)V + 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; + public final fun statementsRequiredToActualizeScheme ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List; + public static synthetic fun statementsRequiredToActualizeScheme$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List; + public final fun withDataBaseLock (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/jvm/functions/Function0;)V +} + +public final class org/jetbrains/exposed/sql/Sequence { + public fun (Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Long;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun createStatement ()Ljava/util/List; + public final fun dropStatement ()Ljava/util/List; + public final fun getCache ()Ljava/lang/Long; + public final fun getCycle ()Ljava/lang/Boolean; + public final fun getDdl ()Ljava/util/List; + public final fun getIdentifier ()Ljava/lang/String; + public final fun getIncrementBy ()Ljava/lang/Long; + public final fun getMaxValue ()Ljava/lang/Long; + public final fun getMinValue ()Ljava/lang/Long; + public final fun getStartWith ()Ljava/lang/Long; +} + +public abstract class org/jetbrains/exposed/sql/SetOperation : org/jetbrains/exposed/sql/AbstractQuery { + public synthetic fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/AbstractQuery;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun count ()J + public fun empty ()Z + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/sql/ResultSet; + public final fun getFirstStatement ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun getOperationName ()Ljava/lang/String; + protected fun getQueryToExecute ()Lorg/jetbrains/exposed/sql/statements/Statement; + public final fun getSecondStatement ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun getSet ()Lorg/jetbrains/exposed/sql/FieldSet; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/QueryBuilder;)Ljava/lang/String; + protected fun prepareStatementSQL (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/SetOperationsKt { + public static final fun except (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/Query;)Lorg/jetbrains/exposed/sql/Except; + public static final fun intersect (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/Query;)Lorg/jetbrains/exposed/sql/Intersect; + public static final fun union (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/Query;)Lorg/jetbrains/exposed/sql/Union; + public static final fun unionAll (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/Query;)Lorg/jetbrains/exposed/sql/UnionAll; +} + +public final class org/jetbrains/exposed/sql/ShortColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Short; +} + +public final class org/jetbrains/exposed/sql/SizedCollection : org/jetbrains/exposed/sql/SizedIterable { + public fun (Ljava/util/Collection;)V + public fun ([Ljava/lang/Object;)V + public fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun count ()J + public fun empty ()Z + public fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun getDelegate ()Ljava/util/Collection; + public fun iterator ()Ljava/util/Iterator; + public fun limit (IJ)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public abstract interface class org/jetbrains/exposed/sql/SizedIterable : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker { + public abstract fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public abstract fun count ()J + public abstract fun empty ()Z + public abstract fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public abstract fun limit (IJ)Lorg/jetbrains/exposed/sql/SizedIterable; + public abstract fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public abstract fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/sql/SizedIterable$DefaultImpls { + public static fun forUpdate (Lorg/jetbrains/exposed/sql/SizedIterable;Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public static synthetic fun forUpdate$default (Lorg/jetbrains/exposed/sql/SizedIterable;Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/SizedIterable; + public static synthetic fun limit$default (Lorg/jetbrains/exposed/sql/SizedIterable;IJILjava/lang/Object;)Lorg/jetbrains/exposed/sql/SizedIterable; + public static fun notForUpdate (Lorg/jetbrains/exposed/sql/SizedIterable;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/sql/Slf4jSqlDebugLogger : org/jetbrains/exposed/sql/SqlLogger { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/Slf4jSqlDebugLogger; + public fun log (Lorg/jetbrains/exposed/sql/statements/StatementContext;Lorg/jetbrains/exposed/sql/Transaction;)V +} + +public final class org/jetbrains/exposed/sql/Slice : org/jetbrains/exposed/sql/FieldSet { + public fun (Lorg/jetbrains/exposed/sql/ColumnSet;Ljava/util/List;)V + public fun getFields ()Ljava/util/List; + public fun getRealFields ()Ljava/util/List; + public fun getSource ()Lorg/jetbrains/exposed/sql/ColumnSet; +} + +public final class org/jetbrains/exposed/sql/SortOrder : java/lang/Enum { + public static final field ASC Lorg/jetbrains/exposed/sql/SortOrder; + public static final field ASC_NULLS_FIRST Lorg/jetbrains/exposed/sql/SortOrder; + public static final field ASC_NULLS_LAST Lorg/jetbrains/exposed/sql/SortOrder; + public static final field DESC Lorg/jetbrains/exposed/sql/SortOrder; + public static final field DESC_NULLS_FIRST Lorg/jetbrains/exposed/sql/SortOrder; + public static final field DESC_NULLS_LAST Lorg/jetbrains/exposed/sql/SortOrder; + public final fun getCode ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/SortOrder; + public static fun values ()[Lorg/jetbrains/exposed/sql/SortOrder; +} + +public final class org/jetbrains/exposed/sql/SqlExpressionBuilder : org/jetbrains/exposed/sql/ISqlExpressionBuilder { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/SqlExpressionBuilder; + public fun asLiteral (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/LiteralOp; + public fun between (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Between; + public fun bitwiseAnd (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/AndBitOp; + public fun bitwiseAnd (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/AndBitOp; + public fun bitwiseOr (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/OrBitOp; + public fun bitwiseOr (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/OrBitOp; + public fun bitwiseXor (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/XorBitOp; + public fun bitwiseXor (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/XorBitOp; + public fun case (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Case; + public fun coalesce (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Coalesce; + public fun concat (Ljava/lang/String;Ljava/util/List;)Lorg/jetbrains/exposed/sql/Concat; + public fun concat ([Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Concat; + public fun cumeDist ()Lorg/jetbrains/exposed/sql/CumeDist; + public fun denseRank ()Lorg/jetbrains/exposed/sql/DenseRank; + public fun div (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/DivideOp; + public fun div (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/DivideOp; + public fun eq (Lorg/jetbrains/exposed/sql/CompositeColumn;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public fun eq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public fun eq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/Op; + public fun eq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public fun eqSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/EqSubQueryOp; + public fun firstValue (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/FirstValue; + public fun greater (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/GreaterOp; + public fun greater (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterOp; + public fun greaterEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterOp; + public fun greaterEq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public fun greaterEq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public fun greaterEqEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/GreaterEqOp; + public fun hasFlag (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/EqOp; + public fun hasFlag (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/EqOp; + public fun inList (Lkotlin/Pair;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun inList (Lkotlin/Triple;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun inList (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun inListIds (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun inSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/InSubQueryOp; + public fun intToDecimal (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/NoOpConversion; + public fun isDistinctFrom (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public fun isDistinctFrom (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public fun isDistinctFromEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsDistinctFromOp; + public fun isNotDistinctFrom (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public fun isNotDistinctFrom (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public fun isNotDistinctFromEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/IsNotDistinctFromOp; + public fun isNotNull (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNotNullOp; + public fun isNull (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/IsNullOp; + public fun lag (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Lag; + public fun lastValue (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LastValue; + public fun lead (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Lead; + public fun less (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LessOp; + public fun less (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessOp; + public fun lessEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessOp; + public fun lessEq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/LessEqOp; + public fun lessEq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessEqOp; + public fun lessEqEntityID (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/LessEqOp; + public fun like (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun like (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun like (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun likeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun likeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun likeWithEntityIDAndExpression (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun match (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Op; + public fun match (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/FunctionProvider$MatchMode;)Lorg/jetbrains/exposed/sql/Op; + public fun minus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/MinusOp; + public fun minus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/MinusOp; + public fun mod (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ModOp; + public fun mod (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ModOp; + public fun modWithEntityId (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun modWithEntityId2 (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun modWithEntityId3 (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun neq (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op; + public fun neq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Comparable;)Lorg/jetbrains/exposed/sql/Op; + public fun neq (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public fun notEqSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotEqSubQueryOp; + public fun notInList (Lkotlin/Pair;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun notInList (Lkotlin/Triple;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun notInList (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun notInListIds (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Iterable;)Lorg/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp; + public fun notInSubQuery (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/NotInSubQueryOp; + public fun notLike (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun notLike (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun notLike (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun notLikeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun notLikeWithEntityID (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/LikePattern;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun notLikeWithEntityIDAndExpression (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/LikeEscapeOp; + public fun nthValue (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/NthValue; + public fun ntile (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/Ntile; + public fun percentRank ()Lorg/jetbrains/exposed/sql/PercentRank; + public fun plus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/PlusOp; + public fun plus (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/PlusOp; + public fun rank ()Lorg/jetbrains/exposed/sql/Rank; + public fun regexp (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/RegexpOp; + public fun regexp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Z)Lorg/jetbrains/exposed/sql/RegexpOp; + public fun rem (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ModOp; + public fun rem (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ModOp; + public fun remWithEntityId (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Number;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun remWithEntityId2 (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun remWithEntityId3 (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/ExpressionWithColumnType; + public fun rowNumber ()Lorg/jetbrains/exposed/sql/RowNumber; + public fun times (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/TimesOp; + public fun times (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/TimesOp; + public fun wrap (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/QueryParameter; +} + +public abstract interface class org/jetbrains/exposed/sql/SqlLogger { + public abstract fun log (Lorg/jetbrains/exposed/sql/statements/StatementContext;Lorg/jetbrains/exposed/sql/Transaction;)V +} + +public final class org/jetbrains/exposed/sql/StdDevPop : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;I)V + public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/StdDevSamp : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;I)V + public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/StdOutSqlLogger : org/jetbrains/exposed/sql/SqlLogger { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/StdOutSqlLogger; + public fun log (Lorg/jetbrains/exposed/sql/statements/StatementContext;Lorg/jetbrains/exposed/sql/Transaction;)V +} + +public abstract class org/jetbrains/exposed/sql/StringColumnType : org/jetbrains/exposed/sql/ColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/StringColumnType$Companion; + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + protected final fun escape (Ljava/lang/String;)Ljava/lang/String; + public final fun getCollate ()Ljava/lang/String; + public fun hashCode ()I + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/StringColumnType$Companion { +} + +public abstract class org/jetbrains/exposed/sql/SubQueryOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression, org/jetbrains/exposed/sql/Op$OpBoolean { + public synthetic fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/AbstractQuery;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getOperator ()Ljava/lang/String; + public final fun getQuery ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Substring : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getLength ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Sum : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnSet, org/jetbrains/exposed/sql/DdlAware { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun autoGenerate (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun autoIncrement (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun autoIncrement$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun autoinc (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun autoinc$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun binary (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun binary (Ljava/lang/String;I)Lorg/jetbrains/exposed/sql/Column; + public final fun blob (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun bool (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun byte (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun char (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun char (Ljava/lang/String;ILjava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun char$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;ILjava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun check (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun check (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun check$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun check$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun clientDefault (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Column; + public fun createStatement ()Ljava/util/List; + public fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun customEnumeration (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun customEnumeration$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun databaseGenerated (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun decimal (Ljava/lang/String;II)Lorg/jetbrains/exposed/sql/Column; + public final fun default (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun default (Lorg/jetbrains/exposed/sql/CompositeColumn;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/CompositeColumn; + public final fun defaultExpression (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Column; + public fun describe (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public final fun double (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public fun dropStatement ()Ljava/util/List; + public final fun entityId (Ljava/lang/String;Lorg/jetbrains/exposed/dao/id/IdTable;)Lorg/jetbrains/exposed/sql/Column; + public final fun entityId (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun entityId (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun enumeration (Ljava/lang/String;Lkotlin/reflect/KClass;)Lorg/jetbrains/exposed/sql/Column; + public final fun enumerationByName (Ljava/lang/String;ILkotlin/reflect/KClass;)Lorg/jetbrains/exposed/sql/Column; + public fun equals (Ljava/lang/Object;)Z + public final fun float (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun foreignKey ([Lkotlin/Pair;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)V + public final fun foreignKey ([Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Table$PrimaryKey;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)V + public static synthetic fun foreignKey$default (Lorg/jetbrains/exposed/sql/Table;[Lkotlin/Pair;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)V + public static synthetic fun foreignKey$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Table$PrimaryKey;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)V + public fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun getAutoIncColumn ()Lorg/jetbrains/exposed/sql/Column; + public fun getColumns ()Ljava/util/List; + public final fun getDdl ()Ljava/util/List; + public final fun getForeignKeys ()Ljava/util/List; + public final fun getIndices ()Ljava/util/List; + public fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; + public final fun getSchemaName ()Ljava/lang/String; + public fun getTableName ()Ljava/lang/String; + public fun hashCode ()I + public final fun index (Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun index (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/Column; + public final fun index (Z[Lorg/jetbrains/exposed/sql/Column;)V + public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Z[Lorg/jetbrains/exposed/sql/Column;ILjava/lang/Object;)V + public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun integer (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public final fun largeText (Ljava/lang/String;Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun largeText$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun long (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun mediumText (Ljava/lang/String;Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun mediumText$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public fun modifyStatement ()Ljava/util/List; + public final fun nameInDatabaseCase ()Ljava/lang/String; + public final fun nameInDatabaseCaseUnquoted ()Ljava/lang/String; + public final fun nullable (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun nullable (Lorg/jetbrains/exposed/sql/CompositeColumn;)Lorg/jetbrains/exposed/sql/CompositeColumn; + public final fun optReference (Ljava/lang/String;Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun optReference (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun optReference$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun optReference$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun optReferenceByIdColumn (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun optReferenceByIdColumn$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun reference (Ljava/lang/String;Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun reference (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun reference$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun reference$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun referenceByIdColumn (Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun referenceByIdColumn$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun references (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public final fun references (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun references$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun referencesById (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun referencesById$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ReferenceOption;Lorg/jetbrains/exposed/sql/ReferenceOption;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun registerColumn (Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;)Lorg/jetbrains/exposed/sql/Column; + public final fun registerCompositeColumn (Lorg/jetbrains/exposed/sql/CompositeColumn;)Lorg/jetbrains/exposed/sql/CompositeColumn; + public final fun replaceColumn (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; + public fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; + public final fun short (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun text (Ljava/lang/String;Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun text$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun ubyte (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun uinteger (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun ulong (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun uniqueIndex (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public final fun uniqueIndex (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun uniqueIndex ([Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun ushort (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun uuid (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun varchar (Ljava/lang/String;ILjava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static synthetic fun varchar$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;ILjava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/Table$Dual : org/jetbrains/exposed/sql/Table { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/Table$Dual; +} + +public final class org/jetbrains/exposed/sql/Table$PrimaryKey { + public fun (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;[Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;[Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getColumns ()[Lorg/jetbrains/exposed/sql/Column; + public final fun getName ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/TableKt { + public static final fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun crossJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun fullJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun innerJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun leftJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun rightJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun targetTables (Lorg/jetbrains/exposed/sql/ColumnSet;)Ljava/util/List; +} + +public class org/jetbrains/exposed/sql/TextColumnType : org/jetbrains/exposed/sql/StringColumnType { + public fun ()V + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getEagerLoading ()Z + public fun preciseType ()Ljava/lang/String; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/TimesOp : org/jetbrains/exposed/sql/CustomOperator { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V +} + +public class org/jetbrains/exposed/sql/Transaction : org/jetbrains/exposed/sql/UserDataHolder, org/jetbrains/exposed/sql/transactions/TransactionInterface { + public static final field Companion Lorg/jetbrains/exposed/sql/Transaction$Companion; + public fun (Lorg/jetbrains/exposed/sql/transactions/TransactionInterface;)V + public fun close ()V + public final fun closeExecutedStatements ()V + public fun commit ()V + public final fun exec (Ljava/lang/String;Ljava/lang/Iterable;Lorg/jetbrains/exposed/sql/statements/StatementType;)Lkotlin/Unit; + public final fun exec (Ljava/lang/String;Ljava/lang/Iterable;Lorg/jetbrains/exposed/sql/statements/StatementType;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public final fun exec (Lorg/jetbrains/exposed/sql/statements/Statement;)Ljava/lang/Object; + public final fun exec (Lorg/jetbrains/exposed/sql/statements/Statement;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static synthetic fun exec$default (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;Ljava/lang/Iterable;Lorg/jetbrains/exposed/sql/statements/StatementType;ILjava/lang/Object;)Lkotlin/Unit; + public static synthetic fun exec$default (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;Ljava/lang/Iterable;Lorg/jetbrains/exposed/sql/statements/StatementType;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public final fun execInBatch (Ljava/util/List;)V + public final fun fullIdentity (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; + public fun getConnection ()Lorg/jetbrains/exposed/sql/statements/api/ExposedConnection; + public final fun getCurrentStatement ()Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + public final fun getDb ()Lorg/jetbrains/exposed/sql/Database; + public final fun getDebug ()Z + public final fun getDuration ()J + public final fun getId ()Ljava/lang/String; + public final fun getMaxRepetitionDelay ()J + public final fun getMinRepetitionDelay ()J + public fun getOuterTransaction ()Lorg/jetbrains/exposed/sql/Transaction; + public fun getReadOnly ()Z + public final fun getRepetitionAttempts ()I + public final fun getStatementCount ()I + public final fun getStatementStats ()Ljava/util/HashMap; + public final fun getStatements ()Ljava/lang/StringBuilder; + public fun getTransactionIsolation ()I + public final fun getWarnLongQueriesDuration ()Ljava/lang/Long; + public final fun identity (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; + public final fun identity (Lorg/jetbrains/exposed/sql/Table;)Ljava/lang/String; + public final fun registerInterceptor (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;)Z + public fun rollback ()V + public final fun setCurrentStatement (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public final fun setDebug (Z)V + public final fun setDuration (J)V + public final fun setMaxRepetitionDelay (J)V + public final fun setMinRepetitionDelay (J)V + public final fun setRepetitionAttempts (I)V + public final fun setStatementCount (I)V + public final fun setWarnLongQueriesDuration (Ljava/lang/Long;)V + public final fun unregisterInterceptor (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;)Z +} + +public final class org/jetbrains/exposed/sql/Transaction$Companion { +} + +public final class org/jetbrains/exposed/sql/Trim : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/UByteColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB-Wa3L5BU (Ljava/lang/Object;)B +} + +public final class org/jetbrains/exposed/sql/UIntegerColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB-OGnWXxg (Ljava/lang/Object;)I +} + +public final class org/jetbrains/exposed/sql/ULongColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB-I7RO_PI (Ljava/lang/Object;)J +} + +public final class org/jetbrains/exposed/sql/UShortColumnType : org/jetbrains/exposed/sql/ColumnType { + public fun ()V + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB-BwKQO78 (Ljava/lang/Object;)S +} + +public final class org/jetbrains/exposed/sql/UUIDColumnType : org/jetbrains/exposed/sql/ColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/UUIDColumnType$Companion; + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/util/UUID; +} + +public final class org/jetbrains/exposed/sql/UUIDColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/UnboundedFollowingWindowFrameBound : org/jetbrains/exposed/sql/UnboundedWindowFrameBound, org/jetbrains/exposed/sql/CurrentOrFollowing { + public fun ()V +} + +public final class org/jetbrains/exposed/sql/UnboundedPrecedingWindowFrameBound : org/jetbrains/exposed/sql/UnboundedWindowFrameBound, org/jetbrains/exposed/sql/CurrentOrPreceding { + public fun ()V +} + +public class org/jetbrains/exposed/sql/UnboundedWindowFrameBound : org/jetbrains/exposed/sql/WindowFrameBound { + public fun (Lorg/jetbrains/exposed/sql/WindowFrameBoundDirection;)V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/Union : org/jetbrains/exposed/sql/SetOperation { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public synthetic fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun copy ()Lorg/jetbrains/exposed/sql/Union; + public synthetic fun withDistinct (Z)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun withDistinct (Z)Lorg/jetbrains/exposed/sql/SetOperation; +} + +public final class org/jetbrains/exposed/sql/UnionAll : org/jetbrains/exposed/sql/SetOperation { + public fun (Lorg/jetbrains/exposed/sql/AbstractQuery;Lorg/jetbrains/exposed/sql/AbstractQuery;)V + public synthetic fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun copy ()Lorg/jetbrains/exposed/sql/UnionAll; + public synthetic fun withDistinct (Z)Lorg/jetbrains/exposed/sql/AbstractQuery; + public fun withDistinct (Z)Lorg/jetbrains/exposed/sql/SetOperation; +} + +public final class org/jetbrains/exposed/sql/UpperCase : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public class org/jetbrains/exposed/sql/UserDataHolder { + public fun ()V + public final fun getOrCreate (Lorg/jetbrains/exposed/sql/Key;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public final fun getUserData (Lorg/jetbrains/exposed/sql/Key;)Ljava/lang/Object; + protected final fun getUserdata ()Ljava/util/concurrent/ConcurrentHashMap; + public final fun putUserData (Lorg/jetbrains/exposed/sql/Key;Ljava/lang/Object;)V + public final fun removeUserData (Lorg/jetbrains/exposed/sql/Key;)Ljava/lang/Object; +} + +public class org/jetbrains/exposed/sql/VarCharColumnType : org/jetbrains/exposed/sql/StringColumnType { + public fun ()V + public fun (ILjava/lang/String;)V + public synthetic fun (ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getColLength ()I + public fun hashCode ()I + public fun preciseType ()Ljava/lang/String; + public fun sqlType ()Ljava/lang/String; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V +} + +public final class org/jetbrains/exposed/sql/VarPop : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;I)V + public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/VarSamp : org/jetbrains/exposed/sql/Function, org/jetbrains/exposed/sql/WindowFunction { + public fun (Lorg/jetbrains/exposed/sql/Expression;I)V + public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract interface class org/jetbrains/exposed/sql/WindowFrameBound { + public static final field Companion Lorg/jetbrains/exposed/sql/WindowFrameBound$Companion; + public abstract fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/WindowFrameBound$Companion { + public final fun currentRow ()Lorg/jetbrains/exposed/sql/CurrentRowWindowFrameBound; + public final fun offsetFollowing (I)Lorg/jetbrains/exposed/sql/OffsetFollowingWindowFrameBound; + public final fun offsetFollowing (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/OffsetFollowingWindowFrameBound; + public final fun offsetPreceding (I)Lorg/jetbrains/exposed/sql/OffsetPrecedingWindowFrameBound; + public final fun offsetPreceding (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/OffsetPrecedingWindowFrameBound; + public final fun unboundedFollowing ()Lorg/jetbrains/exposed/sql/UnboundedFollowingWindowFrameBound; + public final fun unboundedPreceding ()Lorg/jetbrains/exposed/sql/UnboundedPrecedingWindowFrameBound; +} + +public final class org/jetbrains/exposed/sql/WindowFrameBoundDirection : java/lang/Enum { + public static final field FOLLOWING Lorg/jetbrains/exposed/sql/WindowFrameBoundDirection; + public static final field PRECEDING Lorg/jetbrains/exposed/sql/WindowFrameBoundDirection; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/WindowFrameBoundDirection; + public static fun values ()[Lorg/jetbrains/exposed/sql/WindowFrameBoundDirection; +} + +public final class org/jetbrains/exposed/sql/WindowFrameClause { + public fun (Lorg/jetbrains/exposed/sql/WindowFrameUnit;Lorg/jetbrains/exposed/sql/WindowFrameBound;Lorg/jetbrains/exposed/sql/WindowFrameBound;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/WindowFrameUnit;Lorg/jetbrains/exposed/sql/WindowFrameBound;Lorg/jetbrains/exposed/sql/WindowFrameBound;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/WindowFrameUnit : java/lang/Enum { + public static final field GROUPS Lorg/jetbrains/exposed/sql/WindowFrameUnit; + public static final field RANGE Lorg/jetbrains/exposed/sql/WindowFrameUnit; + public static final field ROWS Lorg/jetbrains/exposed/sql/WindowFrameUnit; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/WindowFrameUnit; + public static fun values ()[Lorg/jetbrains/exposed/sql/WindowFrameUnit; +} + +public abstract interface class org/jetbrains/exposed/sql/WindowFunction { + public abstract fun over ()Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public abstract fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/WindowFunctionDefinition : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/WindowFunction;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun groups (Lorg/jetbrains/exposed/sql/CurrentOrPreceding;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun groups (Lorg/jetbrains/exposed/sql/WindowFrameBound;Lorg/jetbrains/exposed/sql/WindowFrameBound;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun orderBy (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/SortOrder;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public static synthetic fun orderBy$default (Lorg/jetbrains/exposed/sql/WindowFunctionDefinition;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/SortOrder;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun partitionBy ([Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun range (Lorg/jetbrains/exposed/sql/CurrentOrPreceding;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun range (Lorg/jetbrains/exposed/sql/WindowFrameBound;Lorg/jetbrains/exposed/sql/WindowFrameBound;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun rows (Lorg/jetbrains/exposed/sql/CurrentOrPreceding;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public final fun rows (Lorg/jetbrains/exposed/sql/WindowFrameBound;Lorg/jetbrains/exposed/sql/WindowFrameBound;)Lorg/jetbrains/exposed/sql/WindowFunctionDefinition; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/XorBitOp : org/jetbrains/exposed/sql/ExpressionWithColumnType { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;)V + public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getExpr1 ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getExpr2 ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/ACosFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/ASinFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/ATanFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/AbsFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/CeilingFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/CosFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/CotFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/DegreesFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/ExpFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/FloorFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/PiFunction : org/jetbrains/exposed/sql/CustomFunction { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/functions/math/PiFunction; +} + +public final class org/jetbrains/exposed/sql/functions/math/PowerFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;II)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/RadiansFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/RoundFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;I)V +} + +public final class org/jetbrains/exposed/sql/functions/math/SignFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/SinFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/SqrtFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public final class org/jetbrains/exposed/sql/functions/math/TanFunction : org/jetbrains/exposed/sql/CustomFunction { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;)V +} + +public abstract class org/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression { + public fun (Ljava/lang/Object;Ljava/lang/Iterable;Z)V + public synthetic fun (Ljava/lang/Object;Ljava/lang/Iterable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected abstract fun extractValues (Ljava/lang/Object;)Ljava/util/List; + protected abstract fun getColumnTypes ()Ljava/util/List; + public final fun getExpr ()Ljava/lang/Object; + public final fun getList ()Ljava/lang/Iterable; + public final fun isInList ()Z + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/ops/PairInListOp : org/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp { + public fun (Lkotlin/Pair;Ljava/lang/Iterable;Z)V + public synthetic fun (Lkotlin/Pair;Ljava/lang/Iterable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun extractValues (Ljava/lang/Object;)Ljava/util/List; +} + +public final class org/jetbrains/exposed/sql/ops/SingleValueInListOp : org/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp { + public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Iterable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/sql/ops/TripleInListOp : org/jetbrains/exposed/sql/ops/InListOrNotInListBaseOp { + public fun (Lkotlin/Triple;Ljava/lang/Iterable;Z)V + public synthetic fun (Lkotlin/Triple;Ljava/lang/Iterable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun extractValues (Ljava/lang/Object;)Ljava/util/List; +} + +public abstract class org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement : org/jetbrains/exposed/sql/statements/InsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;ZZ)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addBatch ()V + protected fun getArguments ()Ljava/util/List; + protected final fun getShouldReturnGeneratedValues ()Z + public fun isAlwaysBatch ()Z + public fun prepared (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + public fun set (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)V + protected fun setArguments (Ljava/util/List;)V + protected fun valuesAndDefaults (Ljava/util/Map;)Ljava/util/Map; +} + +public final class org/jetbrains/exposed/sql/statements/BatchDataInconsistentException : java/lang/Exception { + public fun (Ljava/lang/String;)V +} + +public class org/jetbrains/exposed/sql/statements/BatchInsertStatement : org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;ZZ)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public class org/jetbrains/exposed/sql/statements/BatchReplaceStatement : org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/statements/BatchUpdateStatement : org/jetbrains/exposed/sql/statements/UpdateStatement { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;)V + public final fun addBatch (Lorg/jetbrains/exposed/dao/id/EntityID;)V + public fun arguments ()Ljava/lang/Iterable; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Integer; + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public final fun getData ()Ljava/util/ArrayList; + public fun getFirstDataSet ()Ljava/util/List; + public final fun getTable ()Lorg/jetbrains/exposed/dao/id/IdTable; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; + public fun update (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)Ljava/lang/Void; + public synthetic fun update (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public class org/jetbrains/exposed/sql/statements/BatchUpsertStatement : org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getKeys ()[Lorg/jetbrains/exposed/sql/Column; + public final fun getOnUpdate ()Ljava/util/List; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/statements/DeleteStatement : org/jetbrains/exposed/sql/statements/Statement { + public static final field Companion Lorg/jetbrains/exposed/sql/statements/DeleteStatement$Companion; + public fun (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Op;ZLjava/lang/Integer;Ljava/lang/Long;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Op;ZLjava/lang/Integer;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun arguments ()Ljava/lang/Iterable; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Integer; + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public final fun getLimit ()Ljava/lang/Integer; + public final fun getOffset ()Ljava/lang/Long; + public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun getWhere ()Lorg/jetbrains/exposed/sql/Op; + public final fun isIgnore ()Z + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/statements/DeleteStatement$Companion { + public final fun all (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/Table;)I + public final fun where (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Op;ZLjava/lang/Integer;Ljava/lang/Long;)I + public static synthetic fun where$default (Lorg/jetbrains/exposed/sql/statements/DeleteStatement$Companion;Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Op;ZLjava/lang/Integer;Ljava/lang/Long;ILjava/lang/Object;)I +} + +public abstract interface class org/jetbrains/exposed/sql/statements/GlobalStatementInterceptor : org/jetbrains/exposed/sql/statements/StatementInterceptor { +} + +public final class org/jetbrains/exposed/sql/statements/GlobalStatementInterceptor$DefaultImpls { + public static fun afterCommit (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun afterExecution (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;Ljava/util/List;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public static fun afterRollback (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun afterStatementPrepared (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public static fun beforeCommit (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun beforeExecution (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/StatementContext;)V + public static fun beforeRollback (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun keepUserDataInTransactionStoreOnCommit (Lorg/jetbrains/exposed/sql/statements/GlobalStatementInterceptor;Ljava/util/Map;)Ljava/util/Map; +} + +public class org/jetbrains/exposed/sql/statements/InsertSelectStatement : org/jetbrains/exposed/sql/statements/Statement { + public fun (Ljava/util/List;Lorg/jetbrains/exposed/sql/AbstractQuery;Z)V + public synthetic fun (Ljava/util/List;Lorg/jetbrains/exposed/sql/AbstractQuery;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun arguments ()Ljava/lang/Iterable; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Integer; + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public final fun getColumns ()Ljava/util/List; + public final fun getSelectQuery ()Lorg/jetbrains/exposed/sql/AbstractQuery; + public final fun isIgnore ()Z + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/statements/InsertStatement : org/jetbrains/exposed/sql/statements/UpdateBuilder { + public fun (Lorg/jetbrains/exposed/sql/Table;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun arguments ()Ljava/lang/Iterable; + public fun arguments ()Ljava/util/List; + protected fun execInsertFunction (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)Lkotlin/Pair; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Integer; + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public final fun get (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Object; + public final fun get (Lorg/jetbrains/exposed/sql/CompositeColumn;)Ljava/lang/Object; + protected fun getArguments ()Ljava/util/List; + protected final fun getAutoIncColumns ()Ljava/util/List; + public final fun getInsertedCount ()I + public final fun getOrNull (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Object; + public final fun getResultedValues ()Ljava/util/List; + public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun isIgnore ()Z + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; + public fun prepared (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + protected fun setArguments (Ljava/util/List;)V + public final fun setInsertedCount (I)V + protected final fun toSqlString (Ljava/util/List;Z)Ljava/lang/String; + protected fun valuesAndDefaults (Ljava/util/Map;)Ljava/util/Map; + public static synthetic fun valuesAndDefaults$default (Lorg/jetbrains/exposed/sql/statements/InsertStatement;Ljava/util/Map;ILjava/lang/Object;)Ljava/util/Map; +} + +public class org/jetbrains/exposed/sql/statements/ReplaceStatement : org/jetbrains/exposed/sql/statements/InsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;)V + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/statements/SQLServerBatchInsertStatement : org/jetbrains/exposed/sql/statements/BatchInsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;ZZ)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun arguments ()Ljava/lang/Iterable; + public fun arguments ()Ljava/util/List; + protected fun execInsertFunction (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)Lkotlin/Pair; + public fun isAlwaysBatch ()Z + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public abstract class org/jetbrains/exposed/sql/statements/Statement { + public fun (Lorg/jetbrains/exposed/sql/statements/StatementType;Ljava/util/List;)V + public abstract fun arguments ()Ljava/lang/Iterable; + public final fun execute (Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public abstract fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public final fun getTargets ()Ljava/util/List; + public final fun getType ()Lorg/jetbrains/exposed/sql/statements/StatementType; + public fun isAlwaysBatch ()Z + public abstract fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; + public static synthetic fun prepareSQL$default (Lorg/jetbrains/exposed/sql/statements/Statement;Lorg/jetbrains/exposed/sql/Transaction;ZILjava/lang/Object;)Ljava/lang/String; + public fun prepared (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; +} + +public final class org/jetbrains/exposed/sql/statements/StatementContext { + public fun (Lorg/jetbrains/exposed/sql/statements/Statement;Ljava/lang/Iterable;)V + public final fun getArgs ()Ljava/lang/Iterable; + public final fun getStatement ()Lorg/jetbrains/exposed/sql/statements/Statement; + public final fun sql (Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/statements/StatementGroup : java/lang/Enum { + public static final field DDL Lorg/jetbrains/exposed/sql/statements/StatementGroup; + public static final field DML Lorg/jetbrains/exposed/sql/statements/StatementGroup; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/StatementGroup; + public static fun values ()[Lorg/jetbrains/exposed/sql/statements/StatementGroup; +} + +public abstract interface class org/jetbrains/exposed/sql/statements/StatementInterceptor { + public abstract fun afterCommit (Lorg/jetbrains/exposed/sql/Transaction;)V + public abstract fun afterExecution (Lorg/jetbrains/exposed/sql/Transaction;Ljava/util/List;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public abstract fun afterRollback (Lorg/jetbrains/exposed/sql/Transaction;)V + public abstract fun afterStatementPrepared (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public abstract fun beforeCommit (Lorg/jetbrains/exposed/sql/Transaction;)V + public abstract fun beforeExecution (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/StatementContext;)V + public abstract fun beforeRollback (Lorg/jetbrains/exposed/sql/Transaction;)V + public abstract fun keepUserDataInTransactionStoreOnCommit (Ljava/util/Map;)Ljava/util/Map; +} + +public final class org/jetbrains/exposed/sql/statements/StatementInterceptor$DefaultImpls { + public static fun afterCommit (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun afterExecution (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;Ljava/util/List;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public static fun afterRollback (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun afterStatementPrepared (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public static fun beforeCommit (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun beforeExecution (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/StatementContext;)V + public static fun beforeRollback (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Lorg/jetbrains/exposed/sql/Transaction;)V + public static fun keepUserDataInTransactionStoreOnCommit (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;Ljava/util/Map;)Ljava/util/Map; +} + +public final class org/jetbrains/exposed/sql/statements/StatementKt { + public static final fun expandArgs (Lorg/jetbrains/exposed/sql/statements/StatementContext;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/statements/StatementType : java/lang/Enum { + public static final field ALTER Lorg/jetbrains/exposed/sql/statements/StatementType; + public static final field CREATE Lorg/jetbrains/exposed/sql/statements/StatementType; + public static final field DELETE Lorg/jetbrains/exposed/sql/statements/StatementType; + public static final field DROP Lorg/jetbrains/exposed/sql/statements/StatementType; + public static final field EXEC Lorg/jetbrains/exposed/sql/statements/StatementType; + 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; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/StatementType; + public static fun values ()[Lorg/jetbrains/exposed/sql/statements/StatementType; +} + +public abstract class org/jetbrains/exposed/sql/statements/UpdateBuilder : org/jetbrains/exposed/sql/statements/Statement { + public fun (Lorg/jetbrains/exposed/sql/statements/StatementType;Ljava/util/List;)V + public fun contains (Lorg/jetbrains/exposed/sql/Column;)Z + protected final fun getHasBatchedValues ()Z + protected final fun getValues ()Ljava/util/Map; + public fun set (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)V + public fun set (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)V + public fun set (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Query;)V + public fun set (Lorg/jetbrains/exposed/sql/CompositeColumn;Ljava/lang/Object;)V + protected final fun setHasBatchedValues (Z)V + public final fun setWithEntityIdExpression (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)V + public final fun setWithEntityIdValue (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Comparable;)V + public final fun setWithNullableEntityIdValue (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Comparable;)V + public fun update (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;)V + public fun update (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Expression;)V +} + +public class org/jetbrains/exposed/sql/statements/UpdateStatement : org/jetbrains/exposed/sql/statements/UpdateBuilder { + public fun (Lorg/jetbrains/exposed/sql/ColumnSet;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ColumnSet;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun arguments ()Ljava/lang/Iterable; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Integer; + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public fun getFirstDataSet ()Ljava/util/List; + public final fun getLimit ()Ljava/lang/Integer; + public final fun getTargetsSet ()Lorg/jetbrains/exposed/sql/ColumnSet; + public final fun getWhere ()Lorg/jetbrains/exposed/sql/Op; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/statements/UpsertStatement : org/jetbrains/exposed/sql/statements/InsertStatement { + public fun (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;)V + public final fun getKeys ()[Lorg/jetbrains/exposed/sql/Column; + public final fun getOnUpdate ()Ljava/util/List; + public final fun getWhere ()Lorg/jetbrains/exposed/sql/Op; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/statements/api/ExposedBlob { + public fun (Ljava/io/InputStream;)V + public fun ([B)V + public fun equals (Ljava/lang/Object;)Z + public final fun getBytes ()[B + public final fun getInputStream ()Ljava/io/InputStream; + public fun hashCode ()I + public final fun hexString ()Ljava/lang/String; +} + +public abstract interface class org/jetbrains/exposed/sql/statements/api/ExposedConnection { + public abstract fun close ()V + public abstract fun commit ()V + public abstract fun executeInBatch (Ljava/util/List;)V + public abstract fun getAutoCommit ()Z + public abstract fun getCatalog ()Ljava/lang/String; + public abstract fun getConnection ()Ljava/lang/Object; + public abstract fun getReadOnly ()Z + public abstract fun getSchema ()Ljava/lang/String; + public abstract fun getTransactionIsolation ()I + public abstract fun isClosed ()Z + public abstract fun metadata (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public abstract fun prepareStatement (Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + public abstract fun prepareStatement (Ljava/lang/String;[Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + public abstract fun releaseSavepoint (Lorg/jetbrains/exposed/sql/statements/api/ExposedSavepoint;)V + public abstract fun rollback ()V + public abstract fun rollback (Lorg/jetbrains/exposed/sql/statements/api/ExposedSavepoint;)V + public abstract fun setAutoCommit (Z)V + public abstract fun setCatalog (Ljava/lang/String;)V + public abstract fun setReadOnly (Z)V + public abstract fun setSavepoint (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/ExposedSavepoint; + public abstract fun setSchema (Ljava/lang/String;)V + public abstract fun setTransactionIsolation (I)V +} + +public abstract class org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata { + public fun (Ljava/lang/String;)V + public abstract fun cleanCache ()V + public abstract fun columns ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun existingPrimaryKeys ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun getCurrentScheme ()Ljava/lang/String; + public final fun getDatabase ()Ljava/lang/String; + public abstract fun getDatabaseDialectName ()Ljava/lang/String; + public abstract fun getDatabaseProductVersion ()Ljava/lang/String; + public abstract fun getDefaultIsolationLevel ()I + public abstract fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi; + public abstract fun getSchemaNames ()Ljava/util/List; + public abstract fun getSupportsAlterTableWithAddColumn ()Z + public abstract fun getSupportsMultipleResultSets ()Z + public abstract fun getSupportsSelectForUpdate ()Z + public abstract fun getTableNames ()Ljava/util/Map; + public abstract fun getUrl ()Ljava/lang/String; + public abstract fun getVersion ()Ljava/math/BigDecimal; + public abstract fun resetCurrentScheme ()V + public abstract fun tableConstraints (Ljava/util/List;)Ljava/util/Map; +} + +public abstract class org/jetbrains/exposed/sql/statements/api/ExposedSavepoint { + public fun (Ljava/lang/String;)V + public final fun getName ()Ljava/lang/String; +} + +public abstract class org/jetbrains/exposed/sql/statements/api/IdentifierManagerApi { + public fun ()V + public final fun cutIfNecessaryAndQuote (Ljava/lang/String;)Ljava/lang/String; + protected abstract fun dbKeywords ()Ljava/util/List; + protected abstract fun getExtraNameCharacters ()Ljava/lang/String; + protected final fun getIdentifierLengthLimit ()I + public final fun getKeywords ()Ljava/util/Set; + protected abstract fun getMaxColumnNameLength ()I + protected abstract fun getOracleVersion ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; + public abstract fun getQuoteString ()Ljava/lang/String; + protected abstract fun getSupportsMixedIdentifiers ()Z + protected abstract fun getSupportsMixedQuotedIdentifiers ()Z + public final fun inProperCase (Ljava/lang/String;)Ljava/lang/String; + protected abstract fun isLowerCaseIdentifiers ()Z + protected abstract fun isLowerCaseQuotedIdentifiers ()Z + protected abstract fun isUpperCaseIdentifiers ()Z + protected abstract fun isUpperCaseQuotedIdentifiers ()Z + public final fun needQuotes (Ljava/lang/String;)Z + public final fun quoteIdentifierWhenWrongCaseOrNecessary (Ljava/lang/String;)Ljava/lang/String; + public final fun quoteIfNecessary (Ljava/lang/String;)Ljava/lang/String; + public final fun shouldQuoteIdentifier (Ljava/lang/String;)Z +} + +protected final class org/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion : java/lang/Enum { + public static final field NonOracle Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; + public static final field Oracle11g Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; + public static final field Oracle12_1g Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; + public static final field Oracle12plus Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; + public static fun values ()[Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi$OracleVersion; +} + +public abstract interface class org/jetbrains/exposed/sql/statements/api/PreparedStatementApi { + public abstract fun addBatch ()V + public abstract fun cancel ()V + public abstract fun closeIfPossible ()V + public abstract fun executeBatch ()Ljava/util/List; + public abstract fun executeQuery ()Ljava/sql/ResultSet; + public abstract fun executeUpdate ()I + public abstract fun fillParameters (Ljava/lang/Iterable;)I + public abstract fun getFetchSize ()Ljava/lang/Integer; + public abstract fun getResultSet ()Ljava/sql/ResultSet; + public abstract fun set (ILjava/lang/Object;)V + public abstract fun setFetchSize (Ljava/lang/Integer;)V + public abstract fun setInputStream (ILjava/io/InputStream;)V + public abstract fun setNull (ILorg/jetbrains/exposed/sql/IColumnType;)V +} + +public final class org/jetbrains/exposed/sql/statements/api/PreparedStatementApi$DefaultImpls { + public static fun fillParameters (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Ljava/lang/Iterable;)I +} + +public final class org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManager : org/jetbrains/exposed/sql/transactions/TransactionManager { + public fun (Lorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun bindTransactionToThread (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun currentOrNull ()Lorg/jetbrains/exposed/sql/Transaction; + public fun getDefaultIsolationLevel ()I + public fun getDefaultMaxRepetitionDelay ()J + public fun getDefaultMinRepetitionDelay ()J + public fun getDefaultReadOnly ()Z + public fun getDefaultRepetitionAttempts ()I + public final fun getThreadLocal ()Ljava/lang/ThreadLocal; + public fun newTransaction (IZLorg/jetbrains/exposed/sql/Transaction;)Lorg/jetbrains/exposed/sql/Transaction; + public fun setDefaultIsolationLevel (I)V + public fun setDefaultMaxRepetitionDelay (J)V + 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 { + public static final fun inTopLevelTransaction (IZLorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static synthetic fun inTopLevelTransaction$default (IZLorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun transaction (IZLorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun transaction (Lorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static synthetic fun transaction$default (IZLorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun transaction$default (Lorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/transactions/TransactionApiKt { + public static final fun getTransactionManager (Lorg/jetbrains/exposed/sql/Database;)Lorg/jetbrains/exposed/sql/transactions/TransactionManager; +} + +public abstract interface class org/jetbrains/exposed/sql/transactions/TransactionInterface { + public abstract fun close ()V + public abstract fun commit ()V + public abstract fun getConnection ()Lorg/jetbrains/exposed/sql/statements/api/ExposedConnection; + public abstract fun getDb ()Lorg/jetbrains/exposed/sql/Database; + public abstract fun getOuterTransaction ()Lorg/jetbrains/exposed/sql/Transaction; + public abstract fun getReadOnly ()Z + public abstract fun getTransactionIsolation ()I + public abstract fun rollback ()V +} + +public abstract interface class org/jetbrains/exposed/sql/transactions/TransactionManager { + public static final field Companion Lorg/jetbrains/exposed/sql/transactions/TransactionManager$Companion; + public abstract fun bindTransactionToThread (Lorg/jetbrains/exposed/sql/Transaction;)V + public abstract fun currentOrNull ()Lorg/jetbrains/exposed/sql/Transaction; + public abstract fun getDefaultIsolationLevel ()I + public abstract fun getDefaultMaxRepetitionDelay ()J + public abstract fun getDefaultMinRepetitionDelay ()J + public abstract fun getDefaultReadOnly ()Z + public abstract fun getDefaultRepetitionAttempts ()I + public abstract fun newTransaction (IZLorg/jetbrains/exposed/sql/Transaction;)Lorg/jetbrains/exposed/sql/Transaction; + public abstract fun setDefaultIsolationLevel (I)V + public abstract fun setDefaultMaxRepetitionDelay (J)V + public abstract fun setDefaultMinRepetitionDelay (J)V + public abstract fun setDefaultReadOnly (Z)V + public abstract fun setDefaultRepetitionAttempts (I)V +} + +public final class org/jetbrains/exposed/sql/transactions/TransactionManager$Companion { + public final fun closeAndUnregister (Lorg/jetbrains/exposed/sql/Database;)V + public final fun current ()Lorg/jetbrains/exposed/sql/Transaction; + public final fun currentOrNew (I)Lorg/jetbrains/exposed/sql/Transaction; + public final fun currentOrNull ()Lorg/jetbrains/exposed/sql/Transaction; + public final fun getDefaultDatabase ()Lorg/jetbrains/exposed/sql/Database; + public final fun getManager ()Lorg/jetbrains/exposed/sql/transactions/TransactionManager; + public final fun isInitialized ()Z + public final fun managerFor (Lorg/jetbrains/exposed/sql/Database;)Lorg/jetbrains/exposed/sql/transactions/TransactionManager; + public final fun registerManager (Lorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/transactions/TransactionManager;)V + public final fun resetCurrent (Lorg/jetbrains/exposed/sql/transactions/TransactionManager;)V + public final fun setDefaultDatabase (Lorg/jetbrains/exposed/sql/Database;)V +} + +public final class org/jetbrains/exposed/sql/transactions/TransactionManager$DefaultImpls { + public static synthetic fun newTransaction$default (Lorg/jetbrains/exposed/sql/transactions/TransactionManager;IZLorg/jetbrains/exposed/sql/Transaction;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Transaction; +} + +public final class org/jetbrains/exposed/sql/transactions/TransactionScopeKt { + public static final fun nullableTransactionScope ()Lorg/jetbrains/exposed/sql/transactions/TransactionStore; + public static final fun transactionScope (Lkotlin/jvm/functions/Function1;)Lkotlin/properties/ReadWriteProperty; +} + +public final class org/jetbrains/exposed/sql/transactions/TransactionStore : kotlin/properties/ReadWriteProperty { + public fun ()V + public fun (Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getInit ()Lkotlin/jvm/functions/Function1; + public fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V +} + +public final class org/jetbrains/exposed/sql/transactions/experimental/SuspendedKt { + public static final fun newSuspendedTransaction (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun newSuspendedTransaction$default (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun suspendedTransactionAsync (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun suspendedTransactionAsync$default (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun withSuspendTransaction (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun withSuspendTransaction$default (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/vendors/ColumnMetadata { + public fun (Ljava/lang/String;IZLjava/lang/Integer;ZLjava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()I + public final fun component3 ()Z + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Z + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;IZLjava/lang/Integer;ZLjava/lang/String;)Lorg/jetbrains/exposed/sql/vendors/ColumnMetadata; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/vendors/ColumnMetadata;Ljava/lang/String;IZLjava/lang/Integer;ZLjava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/vendors/ColumnMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getAutoIncrement ()Z + public final fun getDefaultDbValue ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getNullable ()Z + public final fun getSize ()Ljava/lang/Integer; + public final fun getType ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract class org/jetbrains/exposed/sql/vendors/DataTypeProvider { + public fun ()V + public abstract fun binaryType ()Ljava/lang/String; + public fun binaryType (I)Ljava/lang/String; + public fun blobType ()Ljava/lang/String; + public fun booleanFromStringToBoolean (Ljava/lang/String;)Z + public fun booleanToStatementString (Z)Ljava/lang/String; + public fun booleanType ()Ljava/lang/String; + public fun byteType ()Ljava/lang/String; + public fun dateTimeType ()Ljava/lang/String; + public fun dateType ()Ljava/lang/String; + public fun doubleType ()Ljava/lang/String; + public fun floatType ()Ljava/lang/String; + public abstract fun hexToDb (Ljava/lang/String;)Ljava/lang/String; + public fun integerAutoincType ()Ljava/lang/String; + public fun integerType ()Ljava/lang/String; + public fun jsonBType ()Ljava/lang/String; + public fun jsonType ()Ljava/lang/String; + public fun largeTextType ()Ljava/lang/String; + public fun longAutoincType ()Ljava/lang/String; + public fun longType ()Ljava/lang/String; + public fun mediumTextType ()Ljava/lang/String; + public fun precessOrderByClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/SortOrder;)V + public fun processForDefaultValue (Lorg/jetbrains/exposed/sql/Expression;)Ljava/lang/String; + public fun shortType ()Ljava/lang/String; + public fun textType ()Ljava/lang/String; + public fun timeType ()Ljava/lang/String; + public fun timestampWithTimeZoneType ()Ljava/lang/String; + public fun ubyteType ()Ljava/lang/String; + public fun uintegerType ()Ljava/lang/String; + public fun ulongType ()Ljava/lang/String; + public fun ushortType ()Ljava/lang/String; + public fun uuidToDB (Ljava/util/UUID;)Ljava/lang/Object; + public fun uuidType ()Ljava/lang/String; + public fun varcharType (I)Ljava/lang/String; +} + +public abstract interface class org/jetbrains/exposed/sql/vendors/DatabaseDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect$Companion; + public abstract fun addPrimaryKey (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; + public abstract fun allTablesNames ()Ljava/util/List; + public abstract fun catalog (Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public abstract fun checkTableMapping (Lorg/jetbrains/exposed/sql/Table;)Z + public abstract fun columnConstraints ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public abstract fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + public abstract fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public abstract fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public abstract fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public abstract fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; + public abstract fun existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun existingPrimaryKeys ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun getDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; + public abstract fun getDatabase ()Ljava/lang/String; + public abstract fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public abstract fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; + public abstract fun getLikePatternSpecialChars ()Ljava/util/Map; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getNeedsQuotesWhenSymbolsInNames ()Z + public abstract fun getNeedsSequenceToAutoInc ()Z + public abstract fun getRequiresAutoCommitOnCreateDrop ()Z + public abstract fun getSupportsCreateSchema ()Z + public abstract fun getSupportsCreateSequence ()Z + public abstract fun getSupportsDualTableConcept ()Z + public abstract fun getSupportsIfNotExists ()Z + public abstract fun getSupportsMultipleGeneratedKeys ()Z + public abstract fun getSupportsOnUpdate ()Z + public abstract fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z + public abstract fun getSupportsOrderByNullsFirstLast ()Z + public abstract fun getSupportsRestrictReferenceOption ()Z + public abstract fun getSupportsSequenceAsGeneratedKeys ()Z + public abstract fun getSupportsSetDefaultReferenceOption ()Z + public abstract fun getSupportsSubqueryUnions ()Z + 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 + public abstract fun resolveRefOptionFromJdbc (I)Lorg/jetbrains/exposed/sql/ReferenceOption; + public abstract fun schemaExists (Lorg/jetbrains/exposed/sql/Schema;)Z + public abstract fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public abstract fun supportsSelectForUpdate ()Z + public abstract fun tableColumns ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public abstract fun tableExists (Lorg/jetbrains/exposed/sql/Table;)Z +} + +public final class org/jetbrains/exposed/sql/vendors/DatabaseDialect$Companion { +} + +public final class org/jetbrains/exposed/sql/vendors/DatabaseDialect$DefaultImpls { + public static fun catalog (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public static fun checkTableMapping (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Table;)Z + public static fun columnConstraints (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;[Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public static fun createDatabase (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Ljava/lang/String;)Ljava/lang/String; + public static fun createSchema (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public static fun dropDatabase (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Ljava/lang/String;)Ljava/lang/String; + public static fun dropSchema (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; + public static fun existingIndices (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;[Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public static fun existingPrimaryKeys (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;[Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public static fun getDefaultReferenceOption (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Lorg/jetbrains/exposed/sql/ReferenceOption; + public static fun getLikePatternSpecialChars (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Ljava/util/Map; + public static fun getNeedsQuotesWhenSymbolsInNames (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getNeedsSequenceToAutoInc (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getRequiresAutoCommitOnCreateDrop (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsCreateSchema (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsCreateSequence (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsDualTableConcept (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsIfNotExists (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsOnUpdate (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsOnlyIdentifiersInGeneratedKeys (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsOrderByNullsFirstLast (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsRestrictReferenceOption (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsSequenceAsGeneratedKeys (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsSetDefaultReferenceOption (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + public static fun getSupportsSubqueryUnions (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Z + 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 resolveRefOptionFromJdbc (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;I)Lorg/jetbrains/exposed/sql/ReferenceOption; + 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/DatabaseDialectKt { + public static final fun getCurrentDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; +} + +public abstract class org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getQuerySuffix ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$ForUpdate : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$ForUpdate; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$MariaDB { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$MariaDB; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$MariaDB$LockInShareMode : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$MariaDB$LockInShareMode; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$MySQL { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$MySQL; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$MySQL$ForShare : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$MySQL$ForShare; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$MySQL$LockInShareMode : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$MySQL$LockInShareMode; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$Oracle { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$Oracle; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$Oracle$ForUpdateNoWait : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$Oracle$ForUpdateNoWait; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$Oracle$ForUpdateWait : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public fun (I)V +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL; +} + +public class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForKeyShare : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForUpdateBase { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForKeyShare$Companion; + public fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForKeyShare$Companion : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForKeyShare { +} + +public class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForNoKeyUpdate : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForUpdateBase { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForNoKeyUpdate$Companion; + public fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForNoKeyUpdate$Companion : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForNoKeyUpdate { +} + +public class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForShare : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForUpdateBase { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForShare$Companion; + public fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForShare$Companion : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForShare { +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForUpdate : org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForUpdateBase { + public fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$ForUpdateBase : org/jetbrains/exposed/sql/vendors/ForUpdateOption { + public fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE;[Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getQuerySuffix ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE : java/lang/Enum { + public static final field NO_WAIT Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE; + public static final field SKIP_LOCKED Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE; + public final fun getStatement ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE; + public static fun values ()[Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption$PostgreSQL$MODE; +} + +public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider { + public fun ()V + protected final fun appendInsertToUpsertClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;)V + protected final fun appendJoinPartForUpdateClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Join;Lorg/jetbrains/exposed/sql/Transaction;)V + protected final fun appendUpdateToUpsertClause (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;Z)V + public fun cast (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun charLength (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun concat (Ljava/lang/String;Lorg/jetbrains/exposed/sql/QueryBuilder;[Lorg/jetbrains/exposed/sql/Expression;)V + public fun day (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun delete (ZLorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public fun getDEFAULT_VALUE_EXPRESSION ()Ljava/lang/String; + protected final fun getKeyColumnsForUpsert (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;)Ljava/util/List; + public fun groupConcat (Lorg/jetbrains/exposed/sql/GroupConcat;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun hour (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun insert (ZLorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public fun jsonContains (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun jsonExists (Lorg/jetbrains/exposed/sql/Expression;[Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun jsonExtract (Lorg/jetbrains/exposed/sql/Expression;[Ljava/lang/String;ZLorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun locate (Lorg/jetbrains/exposed/sql/QueryBuilder;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)V + public fun match (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/FunctionProvider$MatchMode;)Lorg/jetbrains/exposed/sql/Op; + public static synthetic fun match$default (Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/FunctionProvider$MatchMode;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Op; + public fun minute (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun month (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun nextVal (Lorg/jetbrains/exposed/sql/Sequence;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun queryLimit (IJZ)Ljava/lang/String; + public fun random (Ljava/lang/Integer;)Ljava/lang/String; + public fun regexp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun replace (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; + public static synthetic fun replace$default (Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;ZILjava/lang/Object;)Ljava/lang/String; + public fun second (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun stdDevPop (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun stdDevSamp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun substring (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/String;)V + public static synthetic fun substring$default (Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;Ljava/lang/String;ILjava/lang/Object;)V + public fun update (Lorg/jetbrains/exposed/sql/Join;Ljava/util/List;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public fun update (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/Integer;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public fun upsert (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/util/List;Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Transaction;[Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; + public fun varPop (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun varSamp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V + public fun year (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public abstract interface class org/jetbrains/exposed/sql/vendors/FunctionProvider$MatchMode { + public abstract fun mode ()Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/vendors/H2Dialect : org/jetbrains/exposed/sql/vendors/VendorDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/H2Dialect$Companion; + public fun ()V + public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public fun getDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; + public fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public final fun getDelegatedDialectNameProvider ()Lorg/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider; + public fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; + public final fun getH2Mode ()Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public fun getName ()Ljava/lang/String; + public fun getNeedsSequenceToAutoInc ()Z + public final fun getOriginalDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; + public final fun getOriginalFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; + public fun getSupportsCreateSchema ()Z + public fun getSupportsCreateSequence ()Z + public fun getSupportsDualTableConcept ()Z + public fun getSupportsIfNotExists ()Z + public fun getSupportsMultipleGeneratedKeys ()Z + public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z + public fun getSupportsOrderByNullsFirstLast ()Z + public fun getSupportsSequenceAsGeneratedKeys ()Z + public fun getSupportsSubqueryUnions ()Z + public fun getSupportsTernaryAffectedRowValues ()Z + 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 { +} + +public final class org/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode : java/lang/Enum { + public static final field MariaDB Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public static final field MySQL Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public static final field Oracle Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public static final field PostgreSQL Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public static final field SQLServer Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public static fun values ()[Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; +} + +public final class org/jetbrains/exposed/sql/vendors/H2Kt { + public static final fun getH2Mode (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; +} + +public final class org/jetbrains/exposed/sql/vendors/KeywordsKt { + public static final fun getANSI_SQL_2003_KEYWORDS ()Ljava/util/Set; + public static final fun getVENDORS_KEYWORDS ()Ljava/util/Map; +} + +public final class org/jetbrains/exposed/sql/vendors/MariaDBDialect : org/jetbrains/exposed/sql/vendors/MysqlDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/MariaDBDialect$Companion; + public fun ()V + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + public fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; + public fun getName ()Ljava/lang/String; + public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z + public fun getSupportsSetDefaultReferenceOption ()Z +} + +public final class org/jetbrains/exposed/sql/vendors/MariaDBDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { +} + +public class org/jetbrains/exposed/sql/vendors/MysqlDialect : org/jetbrains/exposed/sql/vendors/VendorDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/MysqlDialect$Companion; + public fun ()V + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; + protected fun fillConstraintCacheForTables (Ljava/util/List;)V + public fun getSupportsCreateSequence ()Z + public fun getSupportsOrderByNullsFirstLast ()Z + public fun getSupportsSetDefaultReferenceOption ()Z + public fun getSupportsSubqueryUnions ()Z + public fun getSupportsTernaryAffectedRowValues ()Z + public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z + 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 { +} + +public class org/jetbrains/exposed/sql/vendors/OracleDialect : org/jetbrains/exposed/sql/vendors/VendorDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/OracleDialect$Companion; + public fun ()V + public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; + public fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun getNeedsQuotesWhenSymbolsInNames ()Z + public fun getNeedsSequenceToAutoInc ()Z + public fun getSupportsDualTableConcept ()Z + public fun getSupportsIfNotExists ()Z + public fun getSupportsMultipleGeneratedKeys ()Z + public fun getSupportsOnUpdate ()Z + public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z + public fun getSupportsOrderByNullsFirstLast ()Z + public fun getSupportsRestrictReferenceOption ()Z + public fun getSupportsSetDefaultReferenceOption ()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 resolveRefOptionFromJdbc (I)Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/vendors/OracleDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { +} + +public class org/jetbrains/exposed/sql/vendors/PostgreSQLDialect : org/jetbrains/exposed/sql/vendors/VendorDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/PostgreSQLDialect$Companion; + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + protected fun createIndexWithType (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public fun getName ()Ljava/lang/String; + public fun getRequiresAutoCommitOnCreateDrop ()Z + 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; +} + +public final class org/jetbrains/exposed/sql/vendors/PostgreSQLDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { +} + +public class org/jetbrains/exposed/sql/vendors/PostgreSQLNGDialect : org/jetbrains/exposed/sql/vendors/PostgreSQLDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/PostgreSQLNGDialect$Companion; + public fun ()V + public fun getRequiresAutoCommitOnCreateDrop ()Z +} + +public final class org/jetbrains/exposed/sql/vendors/PostgreSQLNGDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { +} + +public final class org/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata { + public fun (Ljava/lang/String;Ljava/util/List;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/util/List;)Lorg/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/vendors/PrimaryKeyMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getColumnNames ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class org/jetbrains/exposed/sql/vendors/SQLServerDialect : org/jetbrains/exposed/sql/vendors/VendorDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/SQLServerDialect$Companion; + public fun ()V + public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + protected fun createIndexWithType (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; + public fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun getLikePatternSpecialChars ()Ljava/util/Map; + public fun getNeedsQuotesWhenSymbolsInNames ()Z + public fun getSupportsIfNotExists ()Z + public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z + public fun getSupportsRestrictReferenceOption ()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; +} + +public final class org/jetbrains/exposed/sql/vendors/SQLServerDialect$Companion : org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { +} + +public class org/jetbrains/exposed/sql/vendors/SQLiteDialect : org/jetbrains/exposed/sql/vendors/VendorDialect { + public static final field Companion Lorg/jetbrains/exposed/sql/vendors/SQLiteDialect$Companion; + public fun ()V + public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public fun getSupportsCreateSchema ()Z + public fun getSupportsCreateSequence ()Z + 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 { + public final fun getENABLE_UPDATE_DELETE_LIMIT ()Z +} + +public abstract class org/jetbrains/exposed/sql/vendors/VendorDialect : org/jetbrains/exposed/sql/vendors/DatabaseDialect { + public fun (Ljava/lang/String;Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider;Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;)V + public fun addPrimaryKey (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/String; + public fun allTablesNames ()Ljava/util/List; + public fun catalog (Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; + public fun checkTableMapping (Lorg/jetbrains/exposed/sql/Table;)Z + public fun columnConstraints ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; + protected fun createIndexWithType (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; + public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; + public fun existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + 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; + public fun getDatabase ()Ljava/lang/String; + public fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; + protected final fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi; + public fun getLikePatternSpecialChars ()Ljava/util/Map; + public fun getName ()Ljava/lang/String; + public fun getNeedsQuotesWhenSymbolsInNames ()Z + public fun getNeedsSequenceToAutoInc ()Z + public fun getRequiresAutoCommitOnCreateDrop ()Z + public fun getSupportsCreateSchema ()Z + public fun getSupportsCreateSequence ()Z + public fun getSupportsDualTableConcept ()Z + public fun getSupportsIfNotExists ()Z + public fun getSupportsMultipleGeneratedKeys ()Z + public fun getSupportsOnUpdate ()Z + public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z + public fun getSupportsOrderByNullsFirstLast ()Z + public fun getSupportsRestrictReferenceOption ()Z + public fun getSupportsSequenceAsGeneratedKeys ()Z + public fun getSupportsSetDefaultReferenceOption ()Z + public fun getSupportsSubqueryUnions ()Z + 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 + public fun resetSchemaCaches ()V + public fun resolveRefOptionFromJdbc (I)Lorg/jetbrains/exposed/sql/ReferenceOption; + public fun schemaExists (Lorg/jetbrains/exposed/sql/Schema;)Z + public fun setSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; + public fun supportsSelectForUpdate ()Z + public fun tableColumns ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public fun tableExists (Lorg/jetbrains/exposed/sql/Table;)Z +} + +public abstract class org/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider { + public fun (Ljava/lang/String;)V + public final fun getDialectName ()Ljava/lang/String; +} + diff --git a/exposed-core/build.gradle.kts b/exposed-core/build.gradle.kts index 4641e3e51e..07910bd13e 100644 --- a/exposed-core/build.gradle.kts +++ b/exposed-core/build.gradle.kts @@ -8,6 +8,10 @@ repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(kotlin("stdlib")) api(kotlin("reflect")) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/EntityID.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/EntityID.kt index e84609a7b5..1a181596de 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/EntityID.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/EntityID.kt @@ -2,6 +2,7 @@ package org.jetbrains.exposed.dao.id open class EntityID> protected constructor(val table: IdTable, id: T?) : Comparable> { constructor(id: T, table: IdTable) : this(table, id) + @Suppress("VariableNaming") var _value: Any? = id val value: T get() { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt index a0e4cbc881..6cc93422c0 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt @@ -19,7 +19,6 @@ object EntityIDFunctionProvider { } } - @Suppress("UNCHECKED_CAST") fun > createEntityID(value: T, table: IdTable) = factory.createEntityID(value, table) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt index 82a696a2e4..d8c773d5f6 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/AbstractQuery.kt @@ -27,7 +27,7 @@ abstract class AbstractQuery>(targets: List) : Sized other.fetchSize = fetchSize } - override fun prepareSQL(transaction: Transaction) = prepareSQL(QueryBuilder(true)) + override fun prepareSQL(transaction: Transaction, prepared: Boolean) = prepareSQL(QueryBuilder(prepared)) abstract fun prepareSQL(builder: QueryBuilder): String diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt index 2f080983ca..94d646dbd7 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt @@ -75,7 +75,6 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() { query.set.source.columns.find { it == original }?.clone() as? Column ?: error("Column not found in original table") - @Suppress("UNCHECKED_CAST") operator fun get(original: Expression): Expression { val aliases = query.set.fields.filterIsInstance>() return aliases.find { it == original }?.let { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt index 19762859ae..f8ed5b0d36 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt @@ -3,6 +3,8 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.exceptions.throwUnsupportedException import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.MysqlDialect +import org.jetbrains.exposed.sql.vendors.SQLServerDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect import org.jetbrains.exposed.sql.vendors.currentDialect import org.jetbrains.exposed.sql.vendors.inProperCase @@ -35,14 +37,27 @@ class Column( var defaultValueFun: (() -> T)? = null internal var dbDefaultValue: Expression? = null + fun defaultValueInDb() = dbDefaultValue + + internal var isDatabaseGenerated: Boolean = false + /** Appends the SQL representation of this column to the specified [queryBuilder]. */ override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = TransactionManager.current().fullIdentity(this@Column, queryBuilder) /** Returns the list of DDL statements that create this column. */ val ddl: List get() = createStatement() + /** Returns the column name in proper case. */ fun nameInDatabaseCase(): String = name.inProperCase() + /** + * Returns the column name with wrapping double-quotation characters removed. + * + * **Note** If used with MySQL or MariaDB, the column name is returned unchanged, since these databases use a + * backtick character as the identifier quotation. + */ + fun nameUnquoted(): String = if (currentDialect is MysqlDialect) name else name.trim('\"') + private val isLastColumnInPK: Boolean get() = this == table.primaryKey?.columns?.last() @@ -79,6 +94,7 @@ class Column( internal fun isOneColumnPK(): Boolean = this == table.primaryKey?.columns?.singleOrNull() /** Returns the SQL representation of this column. */ + @Suppress("ComplexMethod") fun descriptionDdl(modify: Boolean = false): String = buildString { val tr = TransactionManager.current() val column = this@Column @@ -109,7 +125,15 @@ class Column( } exposedLogger.error("${currentDialect.name} ${tr.db.version} doesn't support expression '$expressionSQL' as default value.$clientDefault") } else { - append(" DEFAULT $expressionSQL") + if (currentDialect is SQLServerDialect) { + // Create a DEFAULT constraint with an explicit name to facilitate removing it later if needed + val tableName = column.table.tableNameWithoutScheme + val columnName = column.name + val constraintName = "DF_${tableName}_$columnName" + append(" CONSTRAINT $constraintName DEFAULT $expressionSQL") + } else { + append(" DEFAULT $expressionSQL") + } } } 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 14f05ca386..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,8 +15,10 @@ 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 /** * Interface common to all column types. @@ -112,7 +114,8 @@ class AutoIncColumnType( /** Returns the name of the sequence used to generate new values for this auto-increment column. */ val autoincSeq: String? - get() = _autoincSeq.takeIf { currentDialect.supportsCreateSequence } ?: fallbackSeqName.takeIf { currentDialect.needsSequenceToAutoInc } + get() = _autoincSeq.takeIf { currentDialect.supportsCreateSequence } + ?: fallbackSeqName.takeIf { currentDialect.needsSequenceToAutoInc } val nextValExpression: NextVal<*>? get() = nextValValue.takeIf { autoincSeq != null } @@ -155,19 +158,13 @@ class AutoIncColumnType( } /** Returns `true` if this is an auto-increment column, `false` otherwise. */ -val IColumnType.isAutoInc: Boolean get() = this is AutoIncColumnType || (this is EntityIDColumnType<*> && idColumn.columnType.isAutoInc) +val IColumnType.isAutoInc: Boolean + get() = this is AutoIncColumnType || (this is EntityIDColumnType<*> && idColumn.columnType.isAutoInc) /** Returns the name of the auto-increment sequence of this column. */ val Column<*>.autoIncColumnType: AutoIncColumnType? - get() = (columnType as? AutoIncColumnType) ?: (columnType as? EntityIDColumnType<*>)?.idColumn?.columnType as? AutoIncColumnType - -@Deprecated( - message = "Will be removed in upcoming releases. Please use [autoIncColumnType.autoincSeq] instead", - replaceWith = ReplaceWith("this.autoIncColumnType.autoincSeq"), - level = DeprecationLevel.HIDDEN -) -val Column<*>.autoIncSeqName: String? - get() = autoIncColumnType?.autoincSeq + get() = (columnType as? AutoIncColumnType) + ?: (columnType as? EntityIDColumnType<*>)?.idColumn?.columnType as? AutoIncColumnType internal fun IColumnType.rawSqlType(): IColumnType = when { this is AutoIncColumnType -> delegate @@ -239,6 +236,10 @@ class ByteColumnType : ColumnType() { /** * Numeric column for storing unsigned 1-byte integers. + * + * **Note:** If the database being used is not MySQL, MariaDB, or SQL Server, this column will represent the + * database's 2-byte integer type with a check constraint that ensures storage of only values + * between 0 and [UByte.MAX_VALUE] inclusive. */ class UByteColumnType : ColumnType() { override fun sqlType(): String = currentDialect.dataTypeProvider.ubyteType() @@ -246,20 +247,26 @@ class UByteColumnType : ColumnType() { override fun valueFromDB(value: Any): UByte { return when (value) { is UByte -> value - is Byte -> value.takeIf { it >= 0 }?.toUByte() - is Number -> value.toByte().takeIf { it >= 0 }?.toUByte() + is Byte -> value.toUByte() + is Number -> value.toShort().toUByte() is String -> value.toUByte() else -> error("Unexpected value of type Byte: $value of ${value::class.qualifiedName}") - } ?: error("negative value but type is UByte: $value") + } } override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { - val v = if (value is UByte) value.toByte() else value + val v = when (value) { + is UByte -> value.toShort() + else -> value + } super.setParameter(stmt, index, v) } override fun notNullValueToDB(value: Any): Any { - val v = if (value is UByte) value.toByte() else value + val v = when (value) { + is UByte -> value.toShort() + else -> value + } return super.notNullValueToDB(v) } } @@ -279,26 +286,35 @@ class ShortColumnType : ColumnType() { /** * Numeric column for storing unsigned 2-byte integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this column will represent the database's 4-byte + * integer type with a check constraint that ensures storage of only values between 0 and [UShort.MAX_VALUE] inclusive. */ class UShortColumnType : ColumnType() { override fun sqlType(): String = currentDialect.dataTypeProvider.ushortType() override fun valueFromDB(value: Any): UShort { return when (value) { is UShort -> value - is Short -> value.takeIf { it >= 0 }?.toUShort() - is Number -> value.toShort().takeIf { it >= 0 }?.toUShort() + is Short -> value.toUShort() + is Number -> value.toInt().toUShort() is String -> value.toUShort() else -> error("Unexpected value of type Short: $value of ${value::class.qualifiedName}") - } ?: error("negative value but type is UShort: $value") + } } override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { - val v = if (value is UShort) value.toShort() else value + val v = when (value) { + is UShort -> value.toInt() + else -> value + } super.setParameter(stmt, index, v) } override fun notNullValueToDB(value: Any): Any { - val v = if (value is UShort) value.toShort() else value + val v = when (value) { + is UShort -> value.toInt() + else -> value + } return super.notNullValueToDB(v) } } @@ -318,26 +334,36 @@ class IntegerColumnType : ColumnType() { /** * Numeric column for storing unsigned 4-byte integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this column will use the database's + * 8-byte integer type with a check constraint that ensures storage of only values + * between 0 and [UInt.MAX_VALUE] inclusive. */ class UIntegerColumnType : ColumnType() { override fun sqlType(): String = currentDialect.dataTypeProvider.uintegerType() override fun valueFromDB(value: Any): UInt { return when (value) { is UInt -> value - is Int -> value.takeIf { it >= 0 }?.toUInt() - is Number -> value.toLong().takeIf { it >= 0 && it < UInt.MAX_VALUE.toLong() }?.toUInt() + is Int -> value.toUInt() + is Number -> value.toLong().toUInt() is String -> value.toUInt() else -> error("Unexpected value of type Int: $value of ${value::class.qualifiedName}") - } ?: error("negative value but type is UInt: $value") + } } override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { - val v = if (value is UInt) value.toInt() else value + val v = when (value) { + is UInt -> value.toLong() + else -> value + } super.setParameter(stmt, index, v) } override fun notNullValueToDB(value: Any): Any { - val v = if (value is UInt) value.toInt() else value + val v = when (value) { + is UInt -> value.toLong() + else -> value + } return super.notNullValueToDB(v) } } @@ -364,19 +390,35 @@ class ULongColumnType : ColumnType() { return when (value) { is ULong -> value is Long -> value.takeIf { it >= 0 }?.toULong() - is Number -> value.toLong().takeIf { it >= 0 }?.toULong() + is Number -> { + if (currentDialect is MysqlDialect) { + value.toString().toBigInteger().takeIf { + it >= "0".toBigInteger() && it <= ULong.MAX_VALUE.toString().toBigInteger() + }?.toString()?.toULong() + } else { + value.toLong().takeIf { it >= 0 }?.toULong() + } + } is String -> value.toULong() else -> error("Unexpected value of type Long: $value of ${value::class.qualifiedName}") - } ?: error("negative value but type is ULong: $value") + } ?: error("Negative value but type is ULong: $value") } override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { - val v = if (value is ULong) value.toLong() else value + val v = when { + value is ULong && currentDialect is MysqlDialect -> value.toString() + value is ULong -> value.toLong() + else -> value + } super.setParameter(stmt, index, v) } override fun notNullValueToDB(value: Any): Any { - val v = if (value is ULong) value.toLong() else value + val v = when { + value is ULong && currentDialect is MysqlDialect -> value.toString() + value is ULong -> value.toLong() + else -> value + } return super.notNullValueToDB(v) } } @@ -426,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() } @@ -649,11 +691,17 @@ open class TextColumnType(collate: String? = null, val eagerLoading: Boolean = f } } -open class MediumTextColumnType(collate: String? = null, eagerLoading: Boolean = false) : TextColumnType(collate, eagerLoading) { +open class MediumTextColumnType( + collate: String? = null, + eagerLoading: Boolean = false +) : TextColumnType(collate, eagerLoading) { override fun preciseType(): String = currentDialect.dataTypeProvider.mediumTextType() } -open class LargeTextColumnType(collate: String? = null, eagerLoading: Boolean = false) : TextColumnType(collate, eagerLoading) { +open class LargeTextColumnType( + collate: String? = null, + eagerLoading: Boolean = false +) : TextColumnType(collate, eagerLoading) { override fun preciseType(): String = currentDialect.dataTypeProvider.largeTextType() } @@ -739,8 +787,9 @@ class BlobColumnType : ColumnType() { } override fun nonNullValueToString(value: Any): String { - if (value !is ExposedBlob) + if (value !is ExposedBlob) { error("Unexpected value of type Blob: $value of ${value::class.qualifiedName}") + } return currentDialect.dataTypeProvider.hexToDb(value.hexString()) } @@ -790,7 +839,8 @@ class UUIDColumnType : ColumnType() { } companion object { - private val uuidRegexp = Regex("[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", RegexOption.IGNORE_CASE) + private val uuidRegexp = + Regex("[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", RegexOption.IGNORE_CASE) } } @@ -808,10 +858,12 @@ class BooleanColumnType : ColumnType() { else -> value.toString().toBoolean() } - override fun nonNullValueToString(value: Any): String = currentDialect.dataTypeProvider.booleanToStatementString(value as Boolean) + override fun nonNullValueToString(value: Any): String = + currentDialect.dataTypeProvider.booleanToStatementString(value as Boolean) override fun notNullValueToDB(value: Any): Any = when { - value is Boolean && (currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) -> + value is Boolean && + (currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) -> nonNullValueToString(value) else -> value } @@ -877,7 +929,9 @@ class EnumerationNameColumnType>( @Suppress("UNCHECKED_CAST") override fun valueFromDB(value: Any): T = when (value) { - is String -> enumConstants[value] ?: error("$value can't be associated with any from enum ${klass.qualifiedName}") + is String -> { + enumConstants[value] ?: error("$value can't be associated with any from enum ${klass.qualifiedName}") + } is Enum<*> -> value as T else -> error("$value of ${value::class.qualifiedName} is not valid for enum ${klass.qualifiedName}") } @@ -907,6 +961,30 @@ class EnumerationNameColumnType>( } } +/** + * Enumeration column for storing enums of type [T] using the custom SQL type [sql]. + */ +class CustomEnumerationColumnType>( + /** Returns the name of this column type instance. */ + val name: String, + /** Returns the SQL definition used for this column type. */ + val sql: String?, + /** Returns the function that converts a value received from a database to an enumeration instance [T]. */ + val fromDb: (Any) -> T, + /** Returns the function that converts an enumeration instance [T] to a value that will be stored to a database. */ + val toDb: (T) -> Any +) : StringColumnType() { + override fun sqlType(): String = sql ?: error("Column $name should exist in database") + + @Suppress("UNCHECKED_CAST") + override fun valueFromDB(value: Any): T = if (value::class.isSubclassOf(Enum::class)) value as T else fromDb(value) + + @Suppress("UNCHECKED_CAST") + override fun notNullValueToDB(value: Any): Any = toDb(value as T) + + override fun nonNullValueToString(value: Any): String = super.nonNullValueToString(notNullValueToDB(value)) +} + // Date/Time columns /** @@ -915,3 +993,12 @@ class EnumerationNameColumnType>( interface IDateColumnType { val hasTimePart: Boolean } + +// JSON/JSONB columns + +/** + * Marker interface for json/jsonb related column types. + */ +interface JsonColumnMarker { + val usesBinaryFormat: Boolean +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt index 8c29d1afaa..38a64eac85 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt @@ -7,6 +7,7 @@ package org.jetbrains.exposed.sql */ abstract class CompositeColumn : Expression() { internal var nullable: Boolean = false + /** * Parse values from [compositeValue] and return list of real columns with its values * diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt index 991ecd97ac..2784c889f2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt @@ -1,10 +1,12 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.vendors.* +import org.jetbrains.exposed.sql.vendors.DatabaseDialect +import org.jetbrains.exposed.sql.vendors.MysqlDialect +import org.jetbrains.exposed.sql.vendors.SQLiteDialect +import org.jetbrains.exposed.sql.vendors.currentDialect import org.jetbrains.exposed.sql.vendors.currentDialectIfAvailable import org.jetbrains.exposed.sql.vendors.inProperCase -import java.sql.DatabaseMetaData /** * Common interface for database objects that can be created, modified and dropped. @@ -29,20 +31,10 @@ enum class ReferenceOption { CASCADE, SET_NULL, RESTRICT, - NO_ACTION; + NO_ACTION, + SET_DEFAULT; override fun toString(): String = name.replace("_", " ") - - companion object { - /** Returns the corresponding [ReferenceOption] for the specified [refOption] from JDBC. */ - fun resolveRefOptionFromJdbc(refOption: Int): ReferenceOption = when (refOption) { - DatabaseMetaData.importedKeyCascade -> CASCADE - DatabaseMetaData.importedKeySetNull -> SET_NULL - DatabaseMetaData.importedKeyRestrict -> RESTRICT - DatabaseMetaData.importedKeyNoAction -> NO_ACTION - else -> currentDialect.defaultReferenceOption - } - } } /** @@ -104,22 +96,48 @@ data class ForeignKeyConstraint( /** Name of this constraint. */ val fkName: String get() = tx.db.identifierManager.cutIfNecessaryAndQuote( - name ?: "fk_${fromTable.tableNameWithoutSchemeSanitized}_${ - from.joinToString("_") { it.name } - }__${target.joinToString("_") { it.name }}" + name ?: ( + "fk_${fromTable.tableNameWithoutSchemeSanitized}_${from.joinToString("_") { it.name }}__" + + target.joinToString("_") { it.name } + ) ).inProperCase() + internal val foreignKeyPart: String get() = buildString { if (fkName.isNotBlank()) { append("CONSTRAINT $fkName ") } append("FOREIGN KEY ($fromColumns) REFERENCES $targetTableName($targetColumns)") + if (deleteRule != ReferenceOption.NO_ACTION) { - append(" ON DELETE $deleteRule") + if (deleteRule == ReferenceOption.RESTRICT && !currentDialect.supportsRestrictReferenceOption) { + exposedLogger.warn( + "${currentDialect.name} doesn't support FOREIGN KEY with RESTRICT reference option with ON DELETE clause. " + + "Please check your $fromTableName table." + ) + } else if (deleteRule == ReferenceOption.SET_DEFAULT && !currentDialect.supportsSetDefaultReferenceOption) { + exposedLogger.warn( + "${currentDialect.name} doesn't support FOREIGN KEY with SET DEFAULT reference option with ON DELETE clause. " + + "Please check your $fromTableName table." + ) + } else { + append(" ON DELETE $deleteRule") + } } + if (updateRule != ReferenceOption.NO_ACTION) { - if (currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { - exposedLogger.warn("Oracle doesn't support FOREIGN KEY with ON UPDATE clause. Please check your $fromTableName table.") + if (!currentDialect.supportsOnUpdate) { + exposedLogger.warn("${currentDialect.name} doesn't support FOREIGN KEY with ON UPDATE clause. Please check your $fromTableName table.") + } else if (updateRule == ReferenceOption.RESTRICT && !currentDialect.supportsRestrictReferenceOption) { + exposedLogger.warn( + "${currentDialect.name} doesn't support FOREIGN KEY with RESTRICT reference option with ON UPDATE clause. " + + "Please check your $fromTableName table." + ) + } else if (updateRule == ReferenceOption.SET_DEFAULT && !currentDialect.supportsSetDefaultReferenceOption) { + exposedLogger.warn( + "${currentDialect.name} doesn't support FOREIGN KEY with SET DEFAULT reference option with ON UPDATE clause. " + + "Please check your $fromTableName table." + ) } else { append(" ON UPDATE $updateRule") } @@ -161,9 +179,12 @@ data class CheckConstraint( internal val checkPart = "CONSTRAINT $checkName CHECK ($checkOp)" + private val DatabaseDialect.cannotAlterCheckConstraint: Boolean + get() = this is SQLiteDialect || (this as? MysqlDialect)?.isMysql8 == false + override fun createStatement(): List { - return if (currentDialect is MysqlDialect) { - exposedLogger.warn("Creation of CHECK constraints is not currently supported by MySQL") + return if (currentDialect.cannotAlterCheckConstraint) { + exposedLogger.warn("Creation of CHECK constraints is not currently supported by ${currentDialect.name}") listOf() } else { listOf("ALTER TABLE $tableName ADD $checkPart") @@ -173,8 +194,8 @@ data class CheckConstraint( override fun modifyStatement(): List = dropStatement() + createStatement() override fun dropStatement(): List { - return if (currentDialect is MysqlDialect) { - exposedLogger.warn("Deletion of CHECK constraints is not currently supported by MySQL") + return if (currentDialect.cannotAlterCheckConstraint) { + exposedLogger.warn("Deletion of CHECK constraints is not currently supported by ${currentDialect.name}") listOf() } else { listOf("ALTER TABLE $tableName DROP CONSTRAINT $checkName") @@ -193,6 +214,8 @@ data class CheckConstraint( } } +typealias FilterCondition = (SqlExpressionBuilder.() -> Op)? + /** * Represents an index. */ @@ -204,7 +227,13 @@ data class Index( /** Optional custom name for the index. */ val customName: String? = null, /** Optional custom index type (e.g, BTREE or HASH) */ - val indexType: String? = null + val indexType: String? = null, + /** Partial index filter condition */ + val filterCondition: Op? = null, + /** Functions that are part of the index. */ + val functions: List>? = null, + /** Table where the functional index should be defined. */ + val functionsTable: Table? = null ) : DdlAware { /** Table where the index is defined. */ val table: Table @@ -212,24 +241,38 @@ data class Index( /** Name of the index. */ val indexName: String get() = customName ?: buildString { - append(table.nameInDatabaseCase()) + append(table.nameInDatabaseCaseUnquoted()) append('_') - append(columns.joinToString("_") { it.name }.inProperCase()) + append(columns.joinToString("_") { it.name }) + functions?.let { f -> + if (columns.isNotEmpty()) append('_') + append(f.joinToString("_") { it.toString().substringBefore("(").lowercase() }) + } if (unique) { - append("_unique".inProperCase()) + append("_unique") } - } + }.inProperCase() init { - require(columns.isNotEmpty()) { "At least one column is required to create an index" } - val table = columns.distinctBy { it.table }.singleOrNull()?.table - requireNotNull(table) { "Columns from different tables can't persist in one index" } - this.table = table + require(columns.isNotEmpty() || functions?.isNotEmpty() == true) { "At least one column or function is required to create an index" } + val columnsTable = if (columns.isNotEmpty()) { + val table = columns.distinctBy { it.table }.singleOrNull()?.table + requireNotNull(table) { "Columns from different tables can't persist in one index" } + table + } else { + null + } + if (functions?.isNotEmpty() == true) { + requireNotNull(functionsTable) { "functionsTable argument must also be provided if functions are defined to create an index" } + } + this.table = columnsTable ?: functionsTable!! } override fun createStatement(): List = listOf(currentDialect.createIndex(this)) override fun modifyStatement(): List = dropStatement() + createStatement() - override fun dropStatement(): List = listOf(currentDialect.dropIndex(table.nameInDatabaseCase(), indexName)) + override fun dropStatement(): List = listOf( + currentDialect.dropIndex(table.nameInDatabaseCase(), indexName, unique, filterCondition != null || functions != null) + ) /** Returns `true` if the [other] index has the same columns and uniqueness as this index, but a different name, `false` otherwise */ fun onlyNameDiffer(other: Index): Boolean = indexName != other.indexName && columns == other.columns && unique == other.unique 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 c9c6c34852..691a8ca646 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 } } @@ -107,7 +112,7 @@ class Database private constructor( } fun registerDialect(prefix: String, dialect: () -> DatabaseDialect) { - dialects[prefix] = dialect + dialects[prefix.lowercase()] = dialect } fun registerJdbcDriver(prefix: String, driverClassName: String, dialect: String) { @@ -144,26 +149,6 @@ class Database private constructor( ) } - @Deprecated( - level = DeprecationLevel.HIDDEN, - replaceWith = ReplaceWith("connectPool(datasource, setupConnection, manager)"), - message = "Use connectPool instead" - ) - fun connect( - datasource: ConnectionPoolDataSource, - setupConnection: (Connection) -> Unit = {}, - databaseConfig: DatabaseConfig? = null, - manager: (Database) -> TransactionManager = { ThreadLocalTransactionManager(it) } - ): Database { - return doConnect( - explicitVendor = null, - config = databaseConfig, - getNewConnection = { datasource.pooledConnection.connection!! }, - setupConnection = setupConnection, - manager = manager - ) - } - fun connectPool( datasource: ConnectionPoolDataSource, setupConnection: (Connection) -> Unit = {}, @@ -203,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/DatabaseConfig.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt index 474cabb0ed..72cdfd857c 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt @@ -9,18 +9,21 @@ class DatabaseConfig private constructor( val defaultFetchSize: Int?, val defaultIsolationLevel: Int, val defaultRepetitionAttempts: Int, + val defaultMinRepetitionDelay: Long, + val defaultMaxRepetitionDelay: Long, val defaultReadOnly: Boolean, val warnLongQueriesDuration: Long?, val maxEntitiesToStoreInCachePerEntity: Int, val keepLoadedReferencesOutOfTransaction: Boolean, val explicitDialect: DatabaseDialect?, val defaultSchema: Schema?, - val logTooMuchResultSetsThreshold: Int + val logTooMuchResultSetsThreshold: Int, + val preserveKeywordCasing: Boolean, ) { class Builder( /** - * SQLLogger to be used to log all SQL statements. [Slf4jSqlDebugLogger] by default + * SQLLogger to be used to log all SQL statements. [Slf4jSqlDebugLogger] by default. */ var sqlLogger: SqlLogger? = null, /** @@ -32,78 +35,111 @@ class DatabaseConfig private constructor( */ var defaultFetchSize: Int? = null, /** - * Default transaction isolation level. If not specified database-specific level will be used - * Can be overridden on per-transaction level by specifying `transactionIsolation` parameter of `transaction` function - * Check [Database.getDefaultIsolationLevel] for the database defaults + * Default transaction isolation level. If not specified, the database-specific level will be used. + * This can be overridden on a per-transaction level by specifying the `transactionIsolation` parameter of + * the `transaction` function. + * Check [Database.getDefaultIsolationLevel] for the database defaults. */ var defaultIsolationLevel: Int = -1, /** - * How many retries will be made inside any `transaction` block if SQLException happens - * Can be overridden on per-transaction level by specifying `repetitionAttempts` parameter on call - * Default attempts are 3 + * How many retries will be made inside any `transaction` block if SQLException happens. + * This can be overridden on a per-transaction level by specifying the `repetitionAttempts` property in a + * `transaction` block. + * Default attempts are 3. */ var defaultRepetitionAttempts: Int = 3, - /** - * Should all connections/transactions be executed in read-only mode by default or not - * Default state is false + * The minimum number of milliseconds to wait before retrying a transaction if SQLException happens. + * This can be overridden on a per-transaction level by specifying the `minRepetitionDelay` property in a + * `transaction` block. + * Default minimum delay is 0. + */ + var defaultMinRepetitionDelay: Long = 0, + /** + * The maximum number of milliseconds to wait before retrying a transaction if SQLException happens. + * This can be overridden on a per-transaction level by specifying the `maxRepetitionDelay` property in a + * `transaction` block. + * Default maximum delay is 0. + */ + var defaultMaxRepetitionDelay: Long = 0, + /** + * Should all connections/transactions be executed in read-only mode by default or not. + * Default state is false. */ var defaultReadOnly: Boolean = false, /** - * Threshold in milliseconds to log queries which exceed the threshold with WARN level - * No tracing enabled by default - * Can be set on per-transaction level by setting [Transaction.warnLongQueriesDuration] field + * Threshold in milliseconds to log queries which exceed the threshold with WARN level. + * No tracing enabled by default. + * This can be set on a per-transaction level by setting [Transaction.warnLongQueriesDuration] field. */ var warnLongQueriesDuration: Long? = null, /** - * Amount of entities to keep in an EntityCache per an Entity class - * Applicable only when `exposed-dao` module is used - * Can be overridden on per-transaction basis via [EntityCache.maxEntitiesToStore] - * All entities will be kept by default + * Amount of entities to keep in an EntityCache per an Entity class. + * Applicable only when `exposed-dao` module is used. + * This can be overridden on a per-transaction basis via [EntityCache.maxEntitiesToStore]. + * All entities will be kept by default. */ var maxEntitiesToStoreInCachePerEntity: Int = Int.MAX_VALUE, /** - * Turns on "mode" for Exposed DAO to store relations (after they were loaded) - * within the entity that will allow to access them outside the transaction. - * Useful when [eager loading](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading) is used + * Turns on "mode" for Exposed DAO to store relations (after they were loaded) within the entity that will + * allow access to them outside the transaction. + * Useful when [eager loading](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading) is used. */ var keepLoadedReferencesOutOfTransaction: Boolean = false, - /** - * Set the explicit dialect for a database. Can be useful when working with not supported dialects which have the same behavior as the one that Exposed supports + * Set the explicit dialect for a database. + * This can be useful when working with unsupported dialects which have the same behavior as the one that + * Exposed supports. */ var explicitDialect: DatabaseDialect? = null, - /** * Set the default schema for a database. */ var defaultSchema: Schema? = null, - /** * Log too much result sets opened in parallel. - * The error log will contain the stacktrace of the place in the code where new result set occurs, and it exceeds the threshold. - * 0 value means no log needed + * The error log will contain the stacktrace of the place in the code where a new result set occurs, and it + * exceeds the threshold. + * 0 value means no log needed. */ var logTooMuchResultSetsThreshold: Int = 0, + /** + * Toggle whether table and column identifiers that are also keywords should retain their case sensitivity. + * Keeping user-defined case sensitivity (value set to `true`) may become the default in future releases. + */ + @ExperimentalKeywordApi + var preserveKeywordCasing: Boolean = false, ) companion object { operator fun invoke(body: Builder.() -> Unit = {}): DatabaseConfig { val builder = Builder().apply(body) + @OptIn(ExperimentalKeywordApi::class) return DatabaseConfig( sqlLogger = builder.sqlLogger ?: Slf4jSqlDebugLogger, useNestedTransactions = builder.useNestedTransactions, defaultFetchSize = builder.defaultFetchSize, defaultIsolationLevel = builder.defaultIsolationLevel, defaultRepetitionAttempts = builder.defaultRepetitionAttempts, + defaultMinRepetitionDelay = builder.defaultMinRepetitionDelay, + defaultMaxRepetitionDelay = builder.defaultMaxRepetitionDelay, defaultReadOnly = builder.defaultReadOnly, warnLongQueriesDuration = builder.warnLongQueriesDuration, maxEntitiesToStoreInCachePerEntity = builder.maxEntitiesToStoreInCachePerEntity, keepLoadedReferencesOutOfTransaction = builder.keepLoadedReferencesOutOfTransaction, explicitDialect = builder.explicitDialect, defaultSchema = builder.defaultSchema, - logTooMuchResultSetsThreshold = builder.logTooMuchResultSetsThreshold + logTooMuchResultSetsThreshold = builder.logTooMuchResultSetsThreshold, + preserveKeywordCasing = builder.preserveKeywordCasing, ) } } } + +@RequiresOptIn( + message = "This API is experimental and the behavior defined by setting this value to 'true' may become the default " + + "in future releases. Its usage must be marked with '@OptIn(org.jetbrains.exposed.sql.ExperimentalKeywordApi::class)' " + + "or '@org.jetbrains.exposed.sql.ExperimentalKeywordApi'." +) +@Target(AnnotationTarget.PROPERTY) +annotation class ExperimentalKeywordApi 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>() + /** Returns the list of arguments used in this query. */ val args: List> get() = _args @@ -79,21 +80,33 @@ class QueryBuilder( /** Adds the specified sequence of [arguments] as values of the specified [sqlType]. */ fun registerArguments(sqlType: IColumnType, arguments: Iterable) { - fun toString(value: T) = when { - prepared && value is String -> value - else -> sqlType.valueToString(value) - } - - arguments.map { it to toString(it) } - .sortedBy { it.second } - .appendTo { + if (arguments is Collection && arguments.size <= 1) { + // avoid potentially expensive valueToString call unless we need to sort values + arguments.forEach { if (prepared) { - _args.add(sqlType to it.first) + _args.add(sqlType to it) append("?") } else { - append(it.second) + append(sqlType.valueToString(it)) } } + } else { + fun toString(value: T) = when { + prepared && value is String -> value + else -> sqlType.valueToString(value) + } + + arguments.map { it to toString(it) } + .sortedBy { it.second } + .appendTo { + if (prepared) { + _args.add(sqlType to it.first) + append("?") + } else { + append(it.second) + } + } + } } override fun toString(): String = internalBuilder.toString() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt index 3ce7d33d0a..f86e96c371 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt @@ -55,7 +55,7 @@ open class CustomOperator( class Random( /** Returns the seed. */ val seed: Int? = null -) : Function(DecimalColumnType(38, 20)) { +) : Function(DecimalColumnType(precision = 38, scale = 20)) { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { val functionProvider = when (currentDialect.h2Mode) { H2Dialect.H2CompatibilityMode.Oracle, H2Dialect.H2CompatibilityMode.SQLServer -> H2FunctionProvider @@ -73,7 +73,9 @@ class Random( class CharLength( val expr: Expression ) : Function(IntegerColumnType()) { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = currentDialect.functionProvider.charLength(expr, queryBuilder) + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.charLength(expr, queryBuilder) + } } /** @@ -105,7 +107,9 @@ class Concat( /** Returns the expressions being concatenated. */ vararg val expr: Expression<*> ) : Function(TextColumnType()) { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = currentDialect.functionProvider.concat(separator, queryBuilder, expr = expr) + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.concat(separator, queryBuilder, expr = expr) + } } /** @@ -121,7 +125,9 @@ class GroupConcat( /** Returns the order in which the elements of each group are sorted. */ vararg val orderBy: Pair, SortOrder> ) : Function(TextColumnType()) { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = currentDialect.functionProvider.groupConcat(this, queryBuilder) + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.groupConcat(this, queryBuilder) + } } /** @@ -133,7 +139,9 @@ class Substring( /** Returns the length of the substring. */ val length: Expression ) : Function(TextColumnType()) { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = currentDialect.functionProvider.substring(expr, start, length, queryBuilder) + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.substring(expr, start, length, queryBuilder) + } } /** @@ -163,8 +171,12 @@ class Min, in S : T?>( /** Returns the expression from which the minimum value is obtained. */ val expr: Expression, columnType: IColumnType -) : Function(columnType) { +) : Function(columnType), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { append("MIN(", expr, ")") } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -174,8 +186,12 @@ class Max, in S : T?>( /** Returns the expression from which the maximum value is obtained. */ val expr: Expression, columnType: IColumnType -) : Function(columnType) { +) : Function(columnType), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { append("MAX(", expr, ")") } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -185,8 +201,12 @@ class Avg, in S : T?>( /** Returns the expression from which the average is calculated. */ val expr: Expression, scale: Int -) : Function(DecimalColumnType(Int.MAX_VALUE, scale)) { +) : Function(DecimalColumnType(Int.MAX_VALUE, scale)), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { append("AVG(", expr, ")") } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -196,8 +216,12 @@ class Sum( /** Returns the expression from which the sum is calculated. */ val expr: Expression, columnType: IColumnType -) : Function(columnType) { +) : Function(columnType), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { append("SUM(", expr, ")") } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -208,13 +232,17 @@ class Count( val expr: Expression<*>, /** Returns whether only distinct element should be count. */ val distinct: Boolean = false -) : Function(LongColumnType()) { +) : Function(LongColumnType()), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { +"COUNT(" if (distinct) +"DISTINCT " +expr +")" } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(LongColumnType(), this) + } } // Aggregate Functions for Statistics @@ -227,7 +255,7 @@ class StdDevPop( /** Returns the expression from which the population standard deviation is calculated. */ val expression: Expression, scale: Int -) : Function(DecimalColumnType(Int.MAX_VALUE, scale)) { +) : Function(DecimalColumnType(Int.MAX_VALUE, scale)), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder) { queryBuilder { val functionProvider = when (currentDialect.h2Mode) { @@ -237,6 +265,10 @@ class StdDevPop( functionProvider.stdDevPop(expression, this) } } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -247,7 +279,7 @@ class StdDevSamp( /** Returns the expression from which the sample standard deviation is calculated. */ val expression: Expression, scale: Int -) : Function(DecimalColumnType(Int.MAX_VALUE, scale)) { +) : Function(DecimalColumnType(Int.MAX_VALUE, scale)), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder) { queryBuilder { val functionProvider = when (currentDialect.h2Mode) { @@ -257,6 +289,10 @@ class StdDevSamp( functionProvider.stdDevSamp(expression, this) } } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -267,7 +303,7 @@ class VarPop( /** Returns the expression from which the population variance is calculated. */ val expression: Expression, scale: Int -) : Function(DecimalColumnType(Int.MAX_VALUE, scale)) { +) : Function(DecimalColumnType(Int.MAX_VALUE, scale)), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder) { queryBuilder { val functionProvider = when (currentDialect.h2Mode) { @@ -277,6 +313,10 @@ class VarPop( functionProvider.varPop(expression, this) } } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } /** @@ -287,7 +327,7 @@ class VarSamp( /** Returns the expression from which the sample variance is calculated. */ val expression: Expression, scale: Int -) : Function(DecimalColumnType(Int.MAX_VALUE, scale)) { +) : Function(DecimalColumnType(Int.MAX_VALUE, scale)), WindowFunction { override fun toQueryBuilder(queryBuilder: QueryBuilder) { queryBuilder { val functionProvider = when (currentDialect.h2Mode) { @@ -297,6 +337,10 @@ class VarSamp( functionProvider.varSamp(expression, this) } } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(columnType, this) + } } // Sequence Manipulation Functions @@ -304,13 +348,15 @@ class VarSamp( /** * Represents an SQL function that advances the specified [seq] and returns the new value. */ -sealed class NextVal ( +sealed class NextVal( /** Returns the sequence from which the next value is obtained. */ val seq: Sequence, columnType: IColumnType ) : Function(columnType) { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = currentDialect.functionProvider.nextVal(seq, queryBuilder) + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.nextVal(seq, queryBuilder) + } class IntNextVal(seq: Sequence) : NextVal(seq, IntegerColumnType()) class LongNextVal(seq: Sequence) : NextVal(seq, LongColumnType()) @@ -332,27 +378,33 @@ class CaseWhen(val value: Expression<*>?) { return this as CaseWhen } - fun Else(e: Expression): Expression = CaseWhenElse(this, e) + fun Else(e: Expression): ExpressionWithColumnType = CaseWhenElse(this, e) } -class CaseWhenElse(val caseWhen: CaseWhen, val elseResult: Expression) : ExpressionWithColumnType(), ComplexExpression { +class CaseWhenElse( + val caseWhen: CaseWhen, + val elseResult: Expression +) : ExpressionWithColumnType(), ComplexExpression { override val columnType: IColumnType = (elseResult as? ExpressionWithColumnType)?.columnType ?: caseWhen.cases.map { it.second }.filterIsInstance>().firstOrNull()?.columnType ?: BooleanColumnType.INSTANCE - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { - append("CASE ") - if (caseWhen.value != null) { - +caseWhen.value - } + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + queryBuilder { + append("CASE") + if (caseWhen.value != null) { + +" " + +caseWhen.value + } - for ((first, second) in caseWhen.cases) { - append(" WHEN ", first, " THEN ", second) - } + for ((first, second) in caseWhen.cases) { + append(" WHEN ", first, " THEN ", second) + } - append(" ELSE ", elseResult, " END") + append(" ELSE ", elseResult, " END") + } } } @@ -383,5 +435,7 @@ class Cast( val expr: Expression<*>, columnType: IColumnType ) : Function(columnType) { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = currentDialect.functionProvider.cast(expr, columnType, queryBuilder) + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.cast(expr, columnType, queryBuilder) + } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt index c3e0e9cc94..642a1170e0 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt @@ -2,6 +2,7 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.SqlExpressionBuilder.wrap +import org.jetbrains.exposed.sql.statements.api.ExposedBlob import org.jetbrains.exposed.sql.vendors.* import java.math.BigDecimal @@ -35,7 +36,7 @@ abstract class Op : Expression() { object FALSE : Op(), OpBoolean { override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { when { - currentDialect is SQLServerDialect || currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> + currentDialect is SQLServerDialect || currentDialect is OracleDialect || currentDialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> build { booleanLiteral(true) eq booleanLiteral(false) }.toQueryBuilder(this) else -> append(currentDialect.dataTypeProvider.booleanToStatementString(false)) } @@ -525,10 +526,10 @@ class LikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, va } } -@Deprecated("Use LikeEscapeOp", replaceWith = ReplaceWith("LikeEscapeOp(expr1, expr2, true, null)"), level = DeprecationLevel.WARNING) +@Deprecated("Use LikeEscapeOp", replaceWith = ReplaceWith("LikeEscapeOp(expr1, expr2, true, null)"), DeprecationLevel.HIDDEN) class LikeOp(expr1: Expression<*>, expr2: Expression<*>) : ComparisonOp(expr1, expr2, "LIKE") -@Deprecated("Use LikeEscapeOp", replaceWith = ReplaceWith("LikeEscapeOp(expr1, expr2, false, null)"), level = DeprecationLevel.WARNING) +@Deprecated("Use LikeEscapeOp", replaceWith = ReplaceWith("LikeEscapeOp(expr1, expr2, false, null)"), DeprecationLevel.HIDDEN) class NotLikeOp(expr1: Expression<*>, expr2: Expression<*>) : ComparisonOp(expr1, expr2, "NOT LIKE") /** @@ -552,7 +553,7 @@ class RegexpOp( /** * Represents an SQL operator that checks if [query] returns at least one row. */ -class exists( +class Exists( /** Returns the query being checked. */ val query: AbstractQuery<*> ) : Op(), Op.OpBoolean { @@ -563,10 +564,13 @@ class exists( } } +/** Returns an SQL operator that checks if [query] returns at least one row. */ +fun exists(query: AbstractQuery<*>) = Exists(query) + /** * Represents an SQL operator that checks if [query] doesn't returns any row. */ -class notExists( +class NotExists( /** Returns the query being checked. */ val query: AbstractQuery<*> ) : Op(), Op.OpBoolean { @@ -577,6 +581,9 @@ class notExists( } } +/** Returns an SQL operator that checks if [query] doesn't returns any row. */ +fun notExists(query: AbstractQuery<*>) = NotExists(query) + sealed class SubQueryOp( val operator: String, /** Returns the expression compared to each row of the query result. */ @@ -678,7 +685,8 @@ class QueryParameter( } /** Returns the specified [value] as a query parameter with the same type as [column]. */ -fun > idParam(value: EntityID, column: Column>): Expression> = QueryParameter(value, EntityIDColumnType(column)) +fun > idParam(value: EntityID, column: Column>): Expression> = + QueryParameter(value, EntityIDColumnType(column)) /** Returns the specified [value] as a boolean query parameter. */ fun booleanParam(value: Boolean): Expression = QueryParameter(value, BooleanColumnType.INSTANCE) @@ -719,6 +727,9 @@ fun stringParam(value: String): Expression = QueryParameter(value, TextC /** Returns the specified [value] as a decimal query parameter. */ fun decimalParam(value: BigDecimal): Expression = QueryParameter(value, DecimalColumnType(value.precision(), value.scale())) +/** Returns the specified [value] as a blob query parameter. */ +fun blobParam(value: ExposedBlob): Expression = QueryParameter(value, BlobColumnType()) + // Misc. /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt index 5e23b00e18..960e6e6ae5 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Query.kt @@ -228,8 +228,7 @@ open class Query(override var set: FieldSet, where: Op?) : AbstractQuer */ fun Query.andWhere(andPart: SqlExpressionBuilder.() -> Op) = adjustWhere { val expr = Op.build { andPart() } - if (this == null) expr - else this and expr + if (this == null) expr else this and expr } /** @@ -238,8 +237,7 @@ fun Query.andWhere(andPart: SqlExpressionBuilder.() -> Op) = adjustWher */ fun Query.orWhere(orPart: SqlExpressionBuilder.() -> Op) = adjustWhere { val expr = Op.build { orPart() } - if (this == null) expr - else this or expr + if (this == null) expr else this or expr } /** @@ -248,8 +246,7 @@ fun Query.orWhere(orPart: SqlExpressionBuilder.() -> Op) = adjustWhere */ fun Query.andHaving(andPart: SqlExpressionBuilder.() -> Op) = adjustHaving { val expr = Op.build { andPart() } - if (this == null) expr - else this and expr + if (this == null) expr else this and expr } /** @@ -258,6 +255,5 @@ fun Query.andHaving(andPart: SqlExpressionBuilder.() -> Op) = adjustHav */ fun Query.orHaving(orPart: SqlExpressionBuilder.() -> Op) = adjustHaving { val expr = Op.build { orPart() } - if (this == null) expr - else this or expr + if (this == null) expr else this or expr } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt index eeb6d84616..3bada266c2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt @@ -12,23 +12,53 @@ class ResultRow( private val lookUpCache = HashMap, Any?>() /** - * Retrieves value of a given expression on this row. + * Retrieves the value of a given expression on this row. * * @param expression expression to evaluate * @throws IllegalStateException if expression is not in record set or if result value is uninitialized * * @see [getOrNull] to get null in the cases an exception would be thrown */ - operator fun get(expression: Expression): T { + operator fun get(expression: Expression): T = getInternal(expression, checkNullability = true) + + /** + * Sets the value of a given expression on this row. + * + * @param expression expression for which to set the value + * @param value value to be set for the given expression + */ + operator fun set(expression: Expression, value: T) { + setInternal(expression, value) + lookUpCache.remove(expression) + } + + private fun setInternal(expression: Expression, value: T) { + val index = getExpressionIndex(expression) + data[index] = value + } + + fun hasValue(expression: Expression): Boolean = fieldIndex[expression]?.let { data[it] != NotInitializedValue } ?: false + + /** + * Retrieves the value of a given expression on this row. + * Returns null in the cases an exception would be thrown in [get]. + * + * @param expression expression to evaluate + */ + fun getOrNull(expression: Expression): T? = if (hasValue(expression)) getInternal(expression, checkNullability = false) else null + + private fun getInternal(expression: Expression, checkNullability: Boolean): T { if (expression in lookUpCache) return lookUpCache[expression] as T val d = getRaw(expression) - if (d == null && expression is Column<*> && expression.dbDefaultValue != null && !expression.columnType.nullable) { - exposedLogger.warn( - "Column ${TransactionManager.current().fullIdentity(expression)} is marked as not null, " + - "has default db value, but returns null. Possible have to re-read it from DB." - ) + if (checkNullability) { + if (d == null && expression is Column<*> && expression.dbDefaultValue != null && !expression.columnType.nullable) { + exposedLogger.warn( + "Column ${TransactionManager.current().fullIdentity(expression)} is marked as not null, " + + "has default db value, but returns null. Possible have to re-read it from DB." + ) + } } val result = database?.dialect?.let { @@ -40,20 +70,6 @@ class ResultRow( return result } - operator fun set(expression: Expression, value: T) { - setInternal(expression, value) - lookUpCache.remove(expression) - } - - private fun setInternal(expression: Expression, value: T) { - val index = getExpressionIndex(expression) - data[index] = value - } - - fun hasValue(expression: Expression): Boolean = fieldIndex[expression]?.let { data[it] != NotInitializedValue } ?: false - - fun getOrNull(expression: Expression): T? = if (hasValue(expression)) get(expression) else null - @Suppress("UNCHECKED_CAST") private fun rawToColumnValue(raw: T?, expression: Expression): T { return when { @@ -107,9 +123,11 @@ class ResultRow( return ResultRow(fieldsIndex).apply { fieldsIndex.forEach { (field, index) -> val columnType = (field as? ExpressionWithColumnType)?.columnType - val value = if (columnType != null) + val value = if (columnType != null) { columnType.readObject(rs, index + 1) - else rs.getObject(index + 1) + } else { + rs.getObject(index + 1) + } data[index] = value } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt index 95fb740028..2a3a9663a2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt @@ -98,10 +98,6 @@ fun ExpressionWithColumnType.varSamp(scale: Int = 2): VarSamp = // Sequence Manipulation Functions -/** Advances this sequence and returns the new value. */ -@Deprecated("please use [nextIntVal] or [nextLongVal] functions", ReplaceWith("nextIntVal()"), DeprecationLevel.ERROR) -fun Sequence.nextVal(): NextVal = nextIntVal() - /** Advances this sequence and returns the new value. */ fun Sequence.nextIntVal(): NextVal = NextVal.IntNextVal(this) @@ -186,9 +182,6 @@ data class LikePattern( } } -@Deprecated("Implement interface ISqlExpressionBuilder instead inherit this class", level = DeprecationLevel.ERROR) -open class SqlExpressionBuilderClass : ISqlExpressionBuilder - @Suppress("INAPPLICABLE_JVM_NAME", "TooManyFunctions") interface ISqlExpressionBuilder { @@ -496,6 +489,68 @@ interface ISqlExpressionBuilder { caseSensitive: Boolean = true ): RegexpOp = RegexpOp(this, pattern, caseSensitive) + // Window Functions + + /** Returns the number of the current row within its partition, counting from 1. */ + fun rowNumber(): RowNumber = RowNumber() + + /** Returns the rank of the current row, with gaps; that is, the row_number of the first row in its peer group. */ + fun rank(): Rank = Rank() + + /** Returns the rank of the current row, without gaps; this function effectively counts peer groups. */ + fun denseRank(): DenseRank = DenseRank() + + /** + * Returns the relative rank of the current row, that is (rank - 1) / (total partition rows - 1). + * The value thus ranges from 0 to 1 inclusive. + */ + fun percentRank(): PercentRank = PercentRank() + + /** + * Returns the cumulative distribution, that is (number of partition rows preceding or peers with current row) / + * (total partition rows). The value thus ranges from 1/N to 1. + */ + fun cumeDist(): CumeDist = CumeDist() + + /** Returns an integer ranging from 1 to the [numBuckets], dividing the partition as equally as possible. */ + fun ntile(numBuckets: ExpressionWithColumnType): Ntile = Ntile(numBuckets) + + /** + * Returns value evaluated at the row that is [offset] rows before the current row within the partition; + * if there is no such row, instead returns [defaultValue]. + * Both [offset] and [defaultValue] are evaluated with respect to the current row. + */ + fun ExpressionWithColumnType.lag( + offset: ExpressionWithColumnType = intLiteral(1), + defaultValue: ExpressionWithColumnType? = null + ): Lag = Lag(this, offset, defaultValue) + + /** + * Returns value evaluated at the row that is [offset] rows after the current row within the partition; + * if there is no such row, instead returns [defaultValue]. + * Both [offset] and [defaultValue] are evaluated with respect to the current row. + */ + fun ExpressionWithColumnType.lead( + offset: ExpressionWithColumnType = intLiteral(1), + defaultValue: ExpressionWithColumnType? = null + ): Lead = Lead(this, offset, defaultValue) + + /** + * Returns value evaluated at the row that is the first row of the window frame. + */ + fun ExpressionWithColumnType.firstValue(): FirstValue = FirstValue(this) + + /** + * Returns value evaluated at the row that is the last row of the window frame. + */ + fun ExpressionWithColumnType.lastValue(): LastValue = LastValue(this) + + /** + * Returns value evaluated at the row that is the [n]'th row of the window frame + * (counting from 1); null if no such row. + */ + fun ExpressionWithColumnType.nthValue(n: ExpressionWithColumnType): NthValue = NthValue(this, n) + // Conditional Expressions /** Returns the first of its arguments that is not null. */ @@ -537,7 +592,9 @@ interface ISqlExpressionBuilder { * Checks if expressions from triple are equal to elements from [list]. * This syntax is unsupported by SQLite and SQL Server **/ - infix fun Triple, ExpressionWithColumnType, ExpressionWithColumnType>.inList(list: Iterable>): InListOrNotInListBaseOp> = + infix fun Triple, ExpressionWithColumnType, ExpressionWithColumnType>.inList( + list: Iterable> + ): InListOrNotInListBaseOp> = TripleInListOp(this, list, isInList = true) /** Checks if this expression is equals to any element from [list]. */ @@ -556,14 +613,18 @@ interface ISqlExpressionBuilder { * Checks if both expressions are not equal to elements from [list]. * This syntax is unsupported by SQLite and SQL Server **/ - infix fun Pair, ExpressionWithColumnType>.notInList(list: Iterable>): InListOrNotInListBaseOp> = + infix fun Pair, ExpressionWithColumnType>.notInList( + list: Iterable> + ): InListOrNotInListBaseOp> = PairInListOp(this, list, isInList = false) /** * Checks if expressions from triple are not equal to elements from [list]. * This syntax is unsupported by SQLite and SQL Server **/ - infix fun Triple, ExpressionWithColumnType, ExpressionWithColumnType>.notInList(list: Iterable>): InListOrNotInListBaseOp> = + infix fun Triple, ExpressionWithColumnType, ExpressionWithColumnType>.notInList( + list: Iterable> + ): InListOrNotInListBaseOp> = TripleInListOp(this, list, isInList = false) /** Checks if this expression is not equals to any element from [list]. */ @@ -595,7 +656,7 @@ interface ISqlExpressionBuilder { } as QueryParameter /** Returns the specified [value] as a literal of type [T]. */ - @Suppress("UNCHECKED_CAST") + @Suppress("UNCHECKED_CAST", "ComplexMethod") fun ExpressionWithColumnType.asLiteral(value: T): LiteralOp = when (value) { is Boolean -> booleanLiteral(value) is Byte -> byteLiteral(value) @@ -614,7 +675,7 @@ interface ISqlExpressionBuilder { } as LiteralOp fun ExpressionWithColumnType.intToDecimal(): NoOpConversion = - NoOpConversion(this, DecimalColumnType(15, 0)) + NoOpConversion(this, DecimalColumnType(precision = 15, scale = 0)) } /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt index b4e1d0078b..403fa0c649 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt @@ -40,7 +40,6 @@ data class Schema( fun exists(): Boolean = currentDialect.schemaExists(this) fun createStatement(): List { - if (!currentDialect.supportsCreateSchema) { throw UnsupportedByDialectException("The current dialect doesn't support create schema statement", currentDialect) } @@ -64,6 +63,7 @@ data class Schema( return listOf(currentDialect.setSchema(this)) } } + /** Appends both [str1] and [str2] to the receiver [StringBuilder] if [str2] is not `null`. */ internal fun StringBuilder.appendIfNotNull(str1: String, str2: Any?) = apply { if (str2 != null) { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt index 4ba552b393..144e74a2f3 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.* import java.math.BigDecimal +@Suppress("TooManyFunctions") object SchemaUtils { private inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { return if (withLogs) { @@ -19,12 +20,11 @@ object SchemaUtils { private class TableDepthGraph(val tables: Iterable
) { val graph = fetchAllTables().let { tables -> - if (tables.isEmpty()) emptyMap() - else { + if (tables.isEmpty()) { + emptyMap() + } else { tables.associateWith { t -> - t.columns.mapNotNull { c -> - c.referee?.let { it.table to c.columnType.nullable } - }.toMap() + t.foreignKeys.map { it.targetTable } } } } @@ -34,9 +34,7 @@ object SchemaUtils { fun parseTable(table: Table) { if (result.add(table)) { - table.columns.forEach { - it.referee?.table?.let(::parseTable) - } + table.foreignKeys.map { it.targetTable }.forEach(::parseTable) } } tables.forEach(::parseTable) @@ -52,7 +50,7 @@ object SchemaUtils { fun traverse(table: Table) { if (table !in visited) { visited += table - graph.getValue(table).forEach { (t, _) -> + graph.getValue(table).forEach { t -> if (t !in visited) { traverse(t) } @@ -77,7 +75,7 @@ object SchemaUtils { if (table in visited) return false recursion += table visited += table - return if (graph[table]!!.any { traverse(it.key) }) { + return if (graph[table]!!.any { traverse(it) }) { true } else { recursion -= table @@ -89,6 +87,7 @@ object SchemaUtils { } fun sortTablesByReferences(tables: Iterable
) = TableDepthGraph(tables).sorted() + fun checkCycle(vararg tables: Table) = TableDepthGraph(tables.toList()).hasCycle() fun createStatements(vararg tables: Table): List { @@ -118,22 +117,15 @@ object SchemaUtils { } } - @Deprecated( - "Will be removed in upcoming releases. Please use overloaded version instead", - ReplaceWith("createFKey(checkNotNull(reference.foreignKey) { \"${"$"}reference does not reference anything\" })"), - DeprecationLevel.ERROR - ) - fun createFKey(reference: Column<*>): List { - val foreignKey = reference.foreignKey - require(foreignKey != null && (foreignKey.deleteRule != null || foreignKey.updateRule != null)) { "$reference does not reference anything" } - return createFKey(foreignKey) - } - 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 +133,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,35 +146,71 @@ 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 -> processForDefaultValue(exp) + + else -> { + if (column.columnType is JsonColumnMarker) { + val processed = processForDefaultValue(exp) + when (dialect) { + is PostgreSQLDialect -> { + if (column.columnType.usesBinaryFormat) { + processed.replace(Regex("(\"|})(:|,)(\\[|\\{|\")"), "$1$2 $3") + } else { + processed + } + } + + is MariaDBDialect -> processed.trim('\'') + is MysqlDialect -> "_utf8mb4\\'${processed.trim('(', ')', '\'')}\\" + else -> processed.trim('\'') + } + } else { + processForDefaultValue(exp) + } + } + } + } + + is Function<*> -> { + var processed = processForDefaultValue(exp) + if (exp.columnType is IDateColumnType && (processed.startsWith("CURRENT_TIMESTAMP") || processed == "GETDATE()")) { + when (currentDialect) { + is SQLServerDialect -> processed = "getdate" + is MariaDBDialect -> processed = processed.lowercase() + } } + processed } + else -> processForDefaultValue(exp) } } @@ -196,13 +224,17 @@ object SchemaUtils { currentDialect.tableColumns(*tables) } + val existingPrimaryKeys = logTimeSpent("Extracting primary keys", withLogs) { + currentDialect.existingPrimaryKeys(*tables) + } + val dbSupportsAlterTableWithAddColumn = TransactionManager.current().db.supportsAlterTableWithAddColumn for (table in tables) { // create columns val thisTableExistingColumns = existingTablesColumns[table].orEmpty() val existingTableColumns = table.columns.mapNotNull { column -> - val existingColumn = thisTableExistingColumns.find { column.name.equals(it.name, true) } + val existingColumn = thisTableExistingColumns.find { column.nameUnquoted().equals(it.name, true) } if (existingColumn != null) column to existingColumn else null }.toMap() val missingTableColumns = table.columns.filter { it !in existingTableColumns } @@ -211,48 +243,70 @@ 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.nameInDatabaseCase() - 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) } + + // add missing primary key + val missingPK = table.primaryKey?.takeIf { pk -> pk.columns.none { it in missingTableColumns } } + if (missingPK != null && existingPrimaryKeys[table] == null) { + val missingPKName = missingPK.name.takeIf { table.isCustomPKNameDefined() } + statements.add( + currentDialect.addPrimaryKey(table, missingPKName, pkColumns = missingPK.columns) + ) + } } } if (dbSupportsAlterTableWithAddColumn) { - val existingColumnConstraint = logTimeSpent("Extracting column constraints", withLogs) { - currentDialect.columnConstraints(*tables) - } + statements.addAll(addMissingColumnConstraints(*tables, withLogs = withLogs)) + } + + return statements + } + + private fun addMissingColumnConstraints(vararg tables: Table, withLogs: Boolean): List { + val existingColumnConstraint = logTimeSpent("Extracting column constraints", withLogs) { + currentDialect.columnConstraints(*tables) + } + + val foreignKeyConstraints = tables.flatMap { table -> + table.foreignKeys.map { it to existingColumnConstraint[table to it.from]?.firstOrNull() } + } - val foreignKeyConstraints = tables.flatMap { table -> - table.foreignKeys.map { it to existingColumnConstraint[table to it.from]?.firstOrNull() } + val statements = ArrayList() + + for ((foreignKey, existingConstraint) in foreignKeyConstraints) { + if (existingConstraint == null) { + statements.addAll(createFKey(foreignKey)) + continue } - 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 - ) { - statements.addAll(existingConstraint.dropStatement()) - statements.addAll(createFKey(foreignKey)) - } + 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)) } } @@ -300,7 +354,27 @@ object SchemaUtils { "${currentDialect.name} requires autoCommit to be enabled for CREATE DATABASE", exception ) - } else throw exception + } else { + throw exception + } + } + } + + /** + * 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() } } @@ -327,7 +401,9 @@ object SchemaUtils { "${currentDialect.name} requires autoCommit to be enabled for DROP DATABASE", exception ) - } else throw exception + } else { + throw exception + } } } @@ -367,7 +443,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() } @@ -389,7 +468,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 } @@ -426,12 +508,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 { @@ -486,7 +571,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)) } @@ -522,13 +609,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() } } @@ -556,6 +647,7 @@ object SchemaUtils { is MysqlDialect -> { connection.catalog = schema.identifier } + is H2Dialect -> { connection.schema = schema.identifier } @@ -590,18 +682,22 @@ object SchemaUtils { * **Note** that when you are using Mysql or MariaDB, this will fail if you try to drop a schema that * contains a table that is referenced by a table in another schema. * - * @sample org.jetbrains.exposed.sql.tests.shared.SchemaTests + * @sample org.jetbrains.exposed.sql.tests.shared.SchemaTests.testDropSchemaWithCascade * * @param schemas the names of the schema * @param cascade flag to drop schema and all of its objects and all objects that depend on those objects. - * You don't have to specify this option when you are using Mysql or MariaDB - * because whether you specify it or not, all objects in the schema will be dropped. + * **Note** This option is not supported by MySQL, MariaDB, or SQL Server, so all objects in the schema will be + * dropped regardless of the flag's value. * @param inBatch flag to perform schema creation in a single batch */ fun dropSchema(vararg schemas: Schema, cascade: Boolean = false, inBatch: Boolean = false) { if (schemas.isEmpty()) return with(TransactionManager.current()) { - val schemasForDeletion = if (currentDialect.supportsIfNotExists) schemas.distinct() else schemas.distinct().filter { it.exists() } + val schemasForDeletion = if (currentDialect.supportsIfNotExists) { + schemas.distinct() + } else { + schemas.distinct().filter { it.exists() } + } val dropStatements = schemasForDeletion.flatMap { it.dropStatement(cascade) } execStatements(inBatch, dropStatements) 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 f79f21efc5..147ac221b0 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 @@ -6,19 +6,13 @@ import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.exceptions.DuplicateColumnException import org.jetbrains.exposed.sql.statements.api.ExposedBlob import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.vendors.OracleDialect -import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect -import org.jetbrains.exposed.sql.vendors.SQLiteDialect -import org.jetbrains.exposed.sql.vendors.currentDialect -import org.jetbrains.exposed.sql.vendors.currentDialectIfAvailable -import org.jetbrains.exposed.sql.vendors.inProperCase +import org.jetbrains.exposed.sql.vendors.* import java.math.BigDecimal import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KParameter import kotlin.reflect.KProperty1 -import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor @@ -45,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 @@ -180,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() @@ -196,12 +194,15 @@ class Join( onColumn != null && otherColumn != null -> { join(otherTable, joinType, onColumn, otherColumn, additionalConstraint) } + onColumn != null || otherColumn != null -> { error("Can't prepare join on $table and $otherTable when only column from a one side provided.") } + additionalConstraint != null -> { join(otherTable, joinType, emptyList(), additionalConstraint) } + else -> { implicitJoin(otherTable, joinType) } @@ -248,12 +249,18 @@ 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 -> { val cond = fkKeys.filter { it.second.size == 1 }.map { it.first to it.second.single() } join(otherTable, joinType, cond, null) @@ -287,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) { @@ -336,9 +345,15 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { else -> javaClass.name.removePrefix("${javaClass.`package`.name}.").substringAfter('$').removeSuffix("Table") } - internal val tableNameWithoutScheme: String get() = tableName.substringAfter(".") + /** Returns the schema name, or null if one does not exist for this table. */ + val schemaName: String? = if (name.contains(".")) name.substringBeforeLast(".") else null + + 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>() @@ -360,13 +375,31 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { private val checkConstraints = mutableListOf>>() + private val generatedCheckPrefix = "chk_${tableName}_unsigned_" + /** * Returns the table name in proper case. * Should be called within transaction or default [tableName] will be returned. */ fun nameInDatabaseCase(): String = tableName.inProperCase() - override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { append(s.identity(this@Table)) } + /** + * Returns the table name, without schema and in proper case, with wrapping single- and double-quotation characters removed. + * + * **Note** If used with MySQL or MariaDB, the table name is returned unchanged, since these databases use a + * backtick character as the identifier quotation. + */ + fun nameInDatabaseCaseUnquoted(): String = if (currentDialect is MysqlDialect) { + tableNameWithoutScheme.inProperCase() + } else { + tableNameWithoutScheme.inProperCase().trim('\"', '\'') + } + + override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { + append( + s.identity(this@Table) + ) + } // Join operations @@ -391,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. @@ -414,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]. @@ -431,7 +476,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { val name: String by lazy { name ?: "pk_$tableNameWithoutSchemeSanitized" } constructor(firstColumn: Column<*>, vararg columns: Column<*>, name: String? = null) : - this(arrayOf(firstColumn, *columns), name) + this(arrayOf(firstColumn) + columns.asList(), name) init { columns.sortWith(compareBy { !it.columnType.isAutoInc }) @@ -460,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 } @@ -477,26 +526,46 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Creates a numeric column, with the specified [name], for storing 1-byte integers. */ fun byte(name: String): Column = registerColumn(name, ByteColumnType()) - /** Creates a numeric column, with the specified [name], for storing 1-byte unsigned integers. */ - fun ubyte(name: String): Column = registerColumn(name, UByteColumnType()) + /** Creates a numeric column, with the specified [name], for storing 1-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL, MariaDB, or SQL Server, this column will use the + * database's 2-byte integer type with a check constraint that ensures storage of only values + * between 0 and [UByte.MAX_VALUE] inclusive. + */ + fun ubyte(name: String): Column = registerColumn(name, UByteColumnType()).apply { + check("${generatedCheckPrefix}byte_$name") { it.between(0u, UByte.MAX_VALUE) } + } /** Creates a numeric column, with the specified [name], for storing 2-byte integers. */ fun short(name: String): Column = registerColumn(name, ShortColumnType()) - /** Creates a numeric column, with the specified [name], for storing 2-byte unsigned integers. */ - fun ushort(name: String): Column = registerColumn(name, UShortColumnType()) + /** Creates a numeric column, with the specified [name], for storing 2-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this column will use the database's 4-byte + * integer type with a check constraint that ensures storage of only values between 0 and [UShort.MAX_VALUE] inclusive. + */ + fun ushort(name: String): Column = registerColumn(name, UShortColumnType()).apply { + check("$generatedCheckPrefix$name") { it.between(0u, UShort.MAX_VALUE) } + } /** Creates a numeric column, with the specified [name], for storing 4-byte integers. */ fun integer(name: String): Column = registerColumn(name, IntegerColumnType()) - /** Creates a numeric column, with the specified [name], for storing 4-byte unsigned integers. */ - fun uinteger(name: String): Column = registerColumn(name, UIntegerColumnType()) + /** Creates a numeric column, with the specified [name], for storing 4-byte unsigned integers. + * + * **Note:** If the database being used is not MySQL or MariaDB, this column will use the database's + * 8-byte integer type with a check constraint that ensures storage of only values + * between 0 and [UInt.MAX_VALUE] inclusive. + */ + fun uinteger(name: String): Column = registerColumn(name, UIntegerColumnType()).apply { + check("$generatedCheckPrefix$name") { it.between(0u, UInt.MAX_VALUE) } + } /** Creates a numeric column, with the specified [name], for storing 8-byte integers. */ fun long(name: String): Column = registerColumn(name, LongColumnType()) /** Creates a numeric column, with the specified [name], for storing 8-byte unsigned integers. */ - fun ulong(name: String): Column = registerColumn(name, ULongColumnType()) + fun ulong(name: String): Column = registerColumn(name, ULongColumnType()) /** Creates a numeric column, with the specified [name], for storing 4-byte (single precision) floating-point numbers. */ fun float(name: String): Column = registerColumn(name, FloatColumnType()) @@ -514,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 @@ -525,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. @@ -602,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) @@ -622,31 +703,21 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { enumerationByName(name, length, T::class) /** - * Creates an enumeration column with custom SQL type. - * The main usage is to use a database specific type. + * Creates an enumeration column, with the custom SQL type [sql], for storing enums of type [T] using this database-specific type. * - * See [https://github.com/JetBrains/Exposed/wiki/DataTypes#how-to-use-database-enum-types] for more details. + * See [Wiki](https://github.com/JetBrains/Exposed/wiki/DataTypes#how-to-use-database-enum-types) for more details. * - * @param name The column name - * @param sql A SQL definition for the column - * @param fromDb A lambda to convert a value received from a database to an enumeration instance - * @param toDb A lambda to convert an enumeration instance to a value which will be stored to a database + * @param name Name of the column + * @param sql SQL definition for the column + * @param fromDb Function that converts a value received from a database to an enumeration instance [T] + * @param toDb Function that converts an enumeration instance [T] to a value that will be stored to a database */ - @Suppress("UNCHECKED_CAST") fun > customEnumeration( name: String, sql: String? = null, fromDb: (Any) -> T, toDb: (T) -> Any - ): Column = registerColumn( - name, - object : StringColumnType() { - override fun sqlType(): String = sql ?: error("Column $name should exists in database ") - override fun valueFromDB(value: Any): T = if (value::class.isSubclassOf(Enum::class)) value as T else fromDb(value) - override fun notNullValueToDB(value: Any): Any = toDb(value as T) - override fun nonNullValueToString(value: Any): String = super.nonNullValueToString(notNullValueToDB(value)) - } - ) + ): Column = registerColumn(name, CustomEnumerationColumnType(name, sql, fromDb, toDb)) // Auto-generated values @@ -701,6 +772,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { defaultValueFun = defaultValue } + // Potential names: readOnly, generatable, dbGeneratable, dbGenerated, generated, generatedDefault, generatedInDb + fun Column.databaseGenerated(): Column = apply { + isDatabaseGenerated = true + } + /** UUID column will auto generate its value on a client side just before an insert. */ fun Column.autoGenerate(): Column = clientDefault { UUID.randomUUID() } @@ -715,7 +791,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. @@ -790,7 +871,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 } @@ -876,7 +961,6 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * * @see ReferenceOption */ - @Suppress("UNCHECKED_CAST") @JvmName("optReferenceByIdColumn") fun , E : EntityID> optReference( name: String, @@ -931,21 +1015,40 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** * Creates an index. * - * @param columns Columns that compose the index. * @param isUnique Whether the index is unique or not. + * @param columns Columns that compose the index. */ - fun index(isUnique: Boolean = false, vararg columns: Column<*>): Unit = index(null, isUnique, *columns) + fun index(isUnique: Boolean = false, vararg columns: Column<*>) { index(null, isUnique, *columns) } /** * Creates an index. * * @param customIndexName Name of the index. - * @param columns Columns that compose the index. * @param isUnique Whether the index is unique or not. + * @param columns Columns that compose the index. + * @param functions Functions that compose the index. * @param indexType A custom index type (e.g., "BTREE" or "HASH"). + * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun index(customIndexName: String? = null, isUnique: Boolean = false, vararg columns: Column<*>, indexType: String? = null) { - _indices.add(Index(columns.toList(), isUnique, customIndexName, indexType = indexType)) + fun index( + customIndexName: String? = null, + isUnique: Boolean = false, + vararg columns: Column<*>, + functions: List>? = null, + indexType: String? = null, + filterCondition: FilterCondition = null + ) { + _indices.add( + Index( + columns.toList(), + isUnique, + customIndexName, + indexType, + filterCondition?.invoke(SqlExpressionBuilder), + functions, + functions?.let { this } + ) + ) } /** @@ -962,22 +1065,35 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * * @param customIndexName Name of the index. */ - fun Column.uniqueIndex(customIndexName: String? = null): Column = index(customIndexName, true) + fun Column.uniqueIndex(customIndexName: String? = null): Column = + index(customIndexName, true) /** * Creates a unique index. * * @param columns Columns that compose the index. + * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun uniqueIndex(vararg columns: Column<*>): Unit = index(null, true, *columns) + fun uniqueIndex(vararg columns: Column<*>, filterCondition: FilterCondition = null) { + index(null, true, *columns, filterCondition = filterCondition) + } /** * Creates a unique index. * * @param customIndexName Name of the index. * @param columns Columns that compose the index. + * @param functions Functions that compose the index. + * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun uniqueIndex(customIndexName: String? = null, vararg columns: Column<*>): Unit = index(customIndexName, true, *columns) + fun uniqueIndex( + customIndexName: String? = null, + vararg columns: Column<*>, + functions: List>? = null, + filterCondition: FilterCondition = null + ) { + index(customIndexName, true, *columns, functions = functions, filterCondition = filterCondition) + } /** * Creates a composite foreign key. @@ -1026,29 +1142,33 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** * Creates a check constraint in this column. - * @param name The name to identify the constraint, optional. Must be **unique** (case-insensitive) to this table, otherwise, the constraint will - * not be created. All names are [trimmed][String.trim], blank names are ignored and the database engine decides the default name. + * @param name The name to identify the constraint, optional. Must be **unique** (case-insensitive) to this table, + * otherwise, the constraint will not be created. All names are [trimmed][String.trim], blank names are ignored and + * the database engine decides the default name. * @param op The expression against which the newly inserted values will be compared. */ fun Column.check(name: String = "", op: SqlExpressionBuilder.(Column) -> Op): Column = apply { if (name.isEmpty() || table.checkConstraints.none { it.first.equals(name, true) }) { table.checkConstraints.add(name to SqlExpressionBuilder.op(this)) } else { - exposedLogger.warn("A CHECK constraint with name '$name' was ignored because there is already one with that name") + exposedLogger + .warn("A CHECK constraint with name '$name' was ignored because there is already one with that name") } } /** * Creates a check constraint in this table. - * @param name The name to identify the constraint, optional. Must be **unique** (case-insensitive) to this table, otherwise, the constraint will - * not be created. All names are [trimmed][String.trim], blank names are ignored and the database engine decides the default name. + * @param name The name to identify the constraint, optional. Must be **unique** (case-insensitive) to this table, + * otherwise, the constraint will not be created. All names are [trimmed][String.trim], blank names are ignored and + * the database engine decides the default name. * @param op The expression against which the newly inserted values will be compared. */ fun check(name: String = "", op: SqlExpressionBuilder.() -> Op) { if (name.isEmpty() || checkConstraints.none { it.first.equals(name, true) }) { checkConstraints.add(name to SqlExpressionBuilder.op()) } else { - exposedLogger.warn("A CHECK constraint with name '$name' was ignored because there is already one with that name") + exposedLogger + .warn("A CHECK constraint with name '$name' was ignored because there is already one with that name") } } @@ -1072,8 +1192,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { private fun Column.cloneWithAutoInc(idSeqName: String?): Column = when (columnType) { is AutoIncColumnType -> this is ColumnType -> { - this.withColumnType(AutoIncColumnType(columnType, idSeqName, "${tableName}_${name}_seq")) + this.withColumnType( + AutoIncColumnType(columnType, idSeqName, "${tableName?.replace("\"", "")}_${name}_seq") + ) } + else -> error("Unsupported column type for auto-increment $columnType") } @@ -1086,10 +1209,26 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { return primaryKey?.let { primaryKey -> val tr = TransactionManager.current() val constraint = tr.db.identifierManager.cutIfNecessaryAndQuote(primaryKey.name) - return primaryKey.columns.joinToString(prefix = "CONSTRAINT $constraint PRIMARY KEY (", postfix = ")", transform = tr::identity) + return primaryKey.columns + .joinToString(prefix = "CONSTRAINT $constraint PRIMARY KEY (", postfix = ")", transform = tr::identity) } } + @Deprecated( + message = "This will be removed in future releases when the check becomes default in IdentifierManagerApi", + level = DeprecationLevel.WARNING + ) + private fun String.warnIfUnflaggedKeyword() { + val warn = TransactionManager.currentOrNull()?.db?.identifierManager?.isUnflaggedKeyword(this) == true + if (warn) { + exposedLogger.warn( + "Keyword identifier used: '$this'. Case sensitivity may not be kept when quoted by default: '${inProperCase()}'. " + + "To keep case sensitivity, opt-in and set 'preserveKeywordCasing' to true in DatabaseConfig block." + ) + } + } + + @Suppress("CyclomaticComplexMethod") override fun createStatement(): List { val createSequence = autoIncColumn?.autoIncColumnType?.autoincSeq?.let { Sequence( @@ -1109,9 +1248,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { if (currentDialect.supportsIfNotExists) { append("IF NOT EXISTS ") } - append(TransactionManager.current().identity(this@Table)) + append(TransactionManager.current().identity(this@Table).also { tableName.warnIfUnflaggedKeyword() }) if (columns.isNotEmpty()) { - columns.joinTo(this, prefix = " (") { it.descriptionDdl(false) } + columns.joinTo(this, prefix = " (") { column -> + column.descriptionDdl(false).also { column.name.warnIfUnflaggedKeyword() } + } if (columns.any { it.isPrimaryConstraintWillBeDefined }) { primaryKeyConstraint()?.let { append(", $it") } @@ -1122,10 +1263,19 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } if (checkConstraints.isNotEmpty()) { - checkConstraints.mapIndexed { index, (name, op) -> + val filteredChecks = when (currentDialect) { + is MysqlDialect -> checkConstraints.filterNot { (name, _) -> + name.startsWith(generatedCheckPrefix) + } + is SQLServerDialect -> checkConstraints.filterNot { (name, _) -> + name.startsWith("${generatedCheckPrefix}byte_") + } + else -> checkConstraints + }.ifEmpty { null } + filteredChecks?.mapIndexed { index, (name, op) -> val resolvedName = name.ifBlank { "check_${tableName}_$index" } CheckConstraint.from(this@Table, resolvedName, op).checkPart - }.joinTo(this, prefix = ", ") + }?.joinTo(this, prefix = ", ") } append(")") @@ -1141,7 +1291,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { return createSequence + createTable + createConstraint } - override fun modifyStatement(): List = throw UnsupportedOperationException("Use modify on columns and indices") + override fun modifyStatement(): List = + throw UnsupportedOperationException("Use modify on columns and indices") override fun dropStatement(): List { val dropTable = buildString { 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 92086eee9c..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 @@ -8,6 +8,7 @@ import org.jetbrains.exposed.sql.statements.StatementInterceptor import org.jetbrains.exposed.sql.statements.StatementType import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi import org.jetbrains.exposed.sql.transactions.TransactionInterface +import org.jetbrains.exposed.sql.transactions.transactionManager import org.jetbrains.exposed.sql.vendors.inProperCase import java.sql.ResultSet import java.util.* @@ -32,13 +33,25 @@ open class UserDataHolder { fun getOrCreate(key: Key, init: () -> T): T = userdata.getOrPut(key, init) as T } -open class Transaction(private val transactionImpl: TransactionInterface) : UserDataHolder(), TransactionInterface by transactionImpl { +open class Transaction( + private val transactionImpl: TransactionInterface +) : UserDataHolder(), TransactionInterface by transactionImpl { final override val db: Database = transactionImpl.db var statementCount: Int = 0 var duration: Long = 0 var warnLongQueriesDuration: Long? = db.config.warnLongQueriesDuration var debug = false + + /** The number of retries that will be made inside this `transaction` block if SQLException happens */ + var repetitionAttempts: Int = db.transactionManager.defaultRepetitionAttempts + + /** The minimum number of milliseconds to wait before retrying this `transaction` if SQLException happens */ + var minRepetitionDelay: Long = db.transactionManager.defaultMinRepetitionDelay + + /** The maximum number of milliseconds to wait before retrying this `transaction` if SQLException happens */ + var maxRepetitionDelay: Long = db.transactionManager.defaultMaxRepetitionDelay + val id by lazy { UUID.randomUUID().toString() } // currently executing statement. Used to log error properly @@ -91,8 +104,11 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User @Suppress("MagicNumber") private fun describeStatement(delta: Long, stmt: String): String = "[${delta}ms] ${stmt.take(1024)}\n\n" - fun exec(@Language("sql") stmt: String, args: Iterable> = emptyList(), explicitStatementType: StatementType? = null) = - exec(stmt, args, explicitStatementType) { } + fun exec( + @Language("sql") stmt: String, + args: Iterable> = emptyList(), + explicitStatementType: StatementType? = null + ) = exec(stmt, args, explicitStatementType) { } fun exec( @Language("sql") stmt: String, @@ -109,7 +125,7 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User 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 @@ -118,7 +134,7 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User return result?.use { transform(it) } } - override fun prepareSQL(transaction: Transaction): String = stmt + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = stmt override fun arguments(): Iterable>> = listOf(args) }) @@ -149,7 +165,7 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User if (debug) { statements.append(describeStatement(delta, lazySQL.value)) - statementStats.getOrPut(lazySQL.value, { 0 to 0L }).let { (count, time) -> + statementStats.getOrPut(lazySQL.value) { 0 to 0L }.let { (count, time) -> statementStats[lazySQL.value] = (count + 1) to (time + delta) } } @@ -189,11 +205,20 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User executedStatements.clear() } + internal fun getRetryInterval(): Long = if (repetitionAttempts > 0) { + maxOf((maxRepetitionDelay - minRepetitionDelay) / (repetitionAttempts + 1), 1) + } else { + 0 + } + companion object { internal val globalInterceptors = arrayListOf() init { - ServiceLoader.load(GlobalStatementInterceptor::class.java, GlobalStatementInterceptor::class.java.classLoader).forEach { + ServiceLoader.load( + GlobalStatementInterceptor::class.java, + GlobalStatementInterceptor::class.java.classLoader + ).forEach { globalInterceptors.add(it) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt new file mode 100644 index 0000000000..8b14cb39fd --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/WindowFunction.kt @@ -0,0 +1,458 @@ +package org.jetbrains.exposed.sql + +import org.jetbrains.exposed.sql.vendors.currentDialect +import java.math.BigDecimal + +/** Interface for functions that can be used as window functions. */ +interface WindowFunction { + /** Returns window function definition. */ + fun over(): WindowFunctionDefinition + + /** Appends the SQL representation of this function to the specified [queryBuilder]. */ + fun toQueryBuilder(queryBuilder: QueryBuilder) +} + +/** Represents an SQL window function with window definition. */ +@Suppress("TooManyFunctions") +class WindowFunctionDefinition( + override val columnType: IColumnType, + /** Returns the function that definition is used for. */ + private val function: WindowFunction +) : ExpressionWithColumnType() { + /** Returns expressions in PARTITION BY clause. */ + private val partitionExpressions: List> = mutableListOf() + + /** Returns expressions in ORDER BY clause. */ + private val orderByExpressions: List, SortOrder>> = mutableListOf() + + /** Returns window frame clause. */ + private var frameClause: WindowFrameClause? = null + + /** + * Groups the rows of the query by specified [expressions] into partitions, + * which are processed separately by the window function. + */ + fun partitionBy(vararg expressions: Expression<*>): WindowFunctionDefinition = apply { + (partitionExpressions as MutableList).addAll(expressions) + } + + /** + * Defines sorting order by [column] and [order] in which the rows of a partition + * are processed by the window function. + */ + fun orderBy(column: Expression<*>, order: SortOrder = SortOrder.ASC): WindowFunctionDefinition = + orderBy(column to order) + + /** + * Defines sorting order by column and order pairs [order] in which the rows of a partition + * are processed by the window function. + */ + fun orderBy(vararg order: Pair, SortOrder>): WindowFunctionDefinition = apply { + (orderByExpressions as MutableList).addAll(order) + } + + /** + * Defines the set of rows constituting the window frame, which is a subset of the current partition. + * Window frame uses [WindowFrameUnit.ROWS] mode and specified [start] and [end] bounds. + */ + fun rows( + start: WindowFrameBound, + end: WindowFrameBound + ): WindowFunctionDefinition = apply { + frameClause = WindowFrameClause(WindowFrameUnit.ROWS, start, end) + } + + /** + * Defines the set of rows constituting the window frame, which is a subset of the current partition. + * Window frame uses [WindowFrameUnit.ROWS] mode and specified [start] bound. + */ + fun rows( + start: CurrentOrPreceding + ): WindowFunctionDefinition = apply { + frameClause = WindowFrameClause(WindowFrameUnit.ROWS, start, null) + } + + /** + * Defines the set of rows constituting the window frame, which is a subset of the current partition. + * Window frame uses [WindowFrameUnit.RANGE] mode and specified [start] and [end] bounds. + */ + fun range( + start: WindowFrameBound, + end: WindowFrameBound + ): WindowFunctionDefinition = apply { + frameClause = WindowFrameClause(WindowFrameUnit.RANGE, start, end) + } + + /** + * Defines the set of rows constituting the window frame, which is a subset of the current partition. + * Window frame uses [WindowFrameUnit.RANGE] mode and specified [start] bound. + */ + fun range( + start: CurrentOrPreceding + ): WindowFunctionDefinition = apply { + frameClause = WindowFrameClause(WindowFrameUnit.RANGE, start, null) + } + + /** + * Defines the set of rows constituting the window frame, which is a subset of the current partition. + * Window frame uses [WindowFrameUnit.GROUPS] mode and specified [start] and [end] bounds. + */ + fun groups( + start: WindowFrameBound, + end: WindowFrameBound + ): WindowFunctionDefinition = apply { + frameClause = WindowFrameClause(WindowFrameUnit.GROUPS, start, end) + } + + /** + * Defines the set of rows constituting the window frame, which is a subset of the current partition. + * Window frame uses [WindowFrameUnit.GROUPS] mode and specified [start] bound. + */ + fun groups( + start: CurrentOrPreceding, + ): WindowFunctionDefinition = apply { + frameClause = WindowFrameClause(WindowFrameUnit.GROUPS, start, null) + } + + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + function.toQueryBuilder(this) + +" OVER(" + appendPartitionByClause() + appendOrderByClause() + frameClause?.let { + +" " + it.toQueryBuilder(this) + } + +")" + } + + private fun QueryBuilder.appendPartitionByClause() { + if (partitionExpressions.isNotEmpty()) { + +"PARTITION BY " + partitionExpressions.appendTo { + +((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it) + } + } + } + + private fun QueryBuilder.appendOrderByClause() { + if (orderByExpressions.isNotEmpty()) { + +" ORDER BY " + orderByExpressions.appendTo { (expression, sortOrder) -> + currentDialect.dataTypeProvider.precessOrderByClause(this, expression, sortOrder) + } + } + } +} + +/** Represents an SQL window function frame clause */ +class WindowFrameClause( + /** Returns frame unit (also called mode). */ + private val unit: WindowFrameUnit, + /** Returns frame start bound. */ + private val start: WindowFrameBound, + /** Returns frame end bound. */ + private val end: WindowFrameBound? = null +) { + fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append(unit.name, " ") + + if (end != null) { + +"BETWEEN " + start.toQueryBuilder(this) + +" AND " + end.toQueryBuilder(this) + } else { + start.toQueryBuilder(this) + } + } +} + +/** Represents an SQL window function frame unit (also called mode). */ +enum class WindowFrameUnit { + ROWS, + RANGE, + GROUPS +} + +/** Represents an SQL window function frame start and end bound. */ +sealed interface WindowFrameBound { + companion object { + /** Returns UNBOUNDED PRECEDING window function frame bound */ + fun unboundedPreceding(): UnboundedPrecedingWindowFrameBound { + return UnboundedPrecedingWindowFrameBound() + } + + /** Returns UNBOUNDED FOLLOWING window function frame bound */ + fun unboundedFollowing(): UnboundedFollowingWindowFrameBound { + return UnboundedFollowingWindowFrameBound() + } + + /** Returns [offset] PRECEDING window function frame bound */ + fun offsetPreceding(offset: Expression): OffsetPrecedingWindowFrameBound { + return OffsetPrecedingWindowFrameBound(offset) + } + + /** Returns [offset] PRECEDING window function frame bound */ + fun offsetPreceding(offset: Int): OffsetPrecedingWindowFrameBound { + return OffsetPrecedingWindowFrameBound(intLiteral(offset)) + } + + /** Returns [offset] FOLLOWING window function frame bound */ + fun offsetFollowing(offset: Expression): OffsetFollowingWindowFrameBound { + return OffsetFollowingWindowFrameBound(offset) + } + + /** Returns [offset] FOLLOWING window function frame bound */ + fun offsetFollowing(offset: Int): OffsetFollowingWindowFrameBound { + return OffsetFollowingWindowFrameBound(intLiteral(offset)) + } + + /** Returns CURRENT ROW window function frame bound */ + fun currentRow(): CurrentRowWindowFrameBound { + return CurrentRowWindowFrameBound + } + } + + fun toQueryBuilder(queryBuilder: QueryBuilder) +} + +/** Represents an SQL window function frame bound that is CURRENT ROW or one of PRECEDING forms. */ +interface CurrentOrPreceding : WindowFrameBound + +/** Represents an SQL window function frame bound that is CURRENT ROW or one of FOLLOWING forms. */ +interface CurrentOrFollowing : WindowFrameBound + +/** + * Represents UNBOUNDED PRECEDING or FOLLOWING window function frame bound. + * [direction] specifies whether first or last partition row will be used. + */ +open class UnboundedWindowFrameBound( + private val direction: WindowFrameBoundDirection +) : WindowFrameBound { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append("UNBOUNDED ", direction.name) + } +} + +/** Represents UNBOUNDED PRECEDING window function frame bound. */ +class UnboundedPrecedingWindowFrameBound : + UnboundedWindowFrameBound(WindowFrameBoundDirection.PRECEDING), + CurrentOrPreceding + +/** Represents UNBOUNDED FOLLOWING window function frame bound. */ +class UnboundedFollowingWindowFrameBound : + UnboundedWindowFrameBound(WindowFrameBoundDirection.FOLLOWING), + CurrentOrFollowing + +/** + * Represents an [offset] PRECEDING or FOLLOWING window function frame bound. + * [direction] specifies whether previous or next partition rows will be used. + */ +open class OffsetWindowFrameBound( + private val offset: Expression, + private val direction: WindowFrameBoundDirection +) : WindowFrameBound { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append(offset, " ", direction.name) + } +} + +/** Represents [offset] PRECEDING window function frame bound. */ +class OffsetPrecedingWindowFrameBound( + offset: Expression +) : OffsetWindowFrameBound(offset, WindowFrameBoundDirection.PRECEDING), CurrentOrPreceding + +/** Represents [offset] FOLLOWING window function frame bound. */ +class OffsetFollowingWindowFrameBound( + offset: Expression +) : OffsetWindowFrameBound(offset, WindowFrameBoundDirection.FOLLOWING), CurrentOrFollowing + +/** Represents an CURRENT ROW window function frame bound. */ +object CurrentRowWindowFrameBound : WindowFrameBound, CurrentOrPreceding, CurrentOrFollowing { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + +"CURRENT ROW" + } +} + +/** Represents window function frame bound direction. */ +enum class WindowFrameBoundDirection { + PRECEDING, + FOLLOWING +} + +/** Represents an SQL function that returns the number of the current row within its partition, counting from 1. */ +class RowNumber : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + +"ROW_NUMBER()" + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(LongColumnType(), this) + } +} + +/** + * Represents an SQL function that returns the rank of the current row, with gaps; that is, the row_number + * of the first row in its peer group. + */ +class Rank : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + +"RANK()" + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(LongColumnType(), this) + } +} + +/** + * Represents an SQL function that returns the rank of the current row, without gaps; this function effectively + * counts peer groups. + */ +class DenseRank : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + +"DENSE_RANK()" + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(LongColumnType(), this) + } +} + +/** + * Represents an SQL function that returns the relative rank of the current row, that is (rank - 1) / + * (total partition rows - 1). The value thus ranges from 0 to 1 inclusive. + * [scale] represents decimal digits count in the fractional part of result. + */ +class PercentRank(private val scale: Int = 2) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + +"PERCENT_RANK()" + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(DecimalColumnType(Int.MAX_VALUE, scale), this) + } +} + +/** + * Represents an SQL function that Returns the cumulative distribution, that is (number of partition rows preceding + * or peers with current row) / (total partition rows). The value thus ranges from 1/N to 1. + * [scale] represents decimal digits count in the fractional part of result. + */ +class CumeDist(private val scale: Int = 2) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + +"CUME_DIST()" + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(DecimalColumnType(Int.MAX_VALUE, scale), this) + } +} + +/** Returns an integer ranging from 1 to the argument value, dividing the partition as equally as possible. */ +class Ntile( + /** Returns number of buckets. */ + val numBuckets: ExpressionWithColumnType +) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + append("NTILE(", numBuckets, ")") + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(IntegerColumnType(), this) + } +} + +/** + * Represents an SQL function that returns value evaluated at the row that is [offset] rows before the current row + * within the partition; if there is no such row, instead returns [defaultValue]. + */ +class Lag( + /** Returns the expression from which the rows are counted. */ + val expr: ExpressionWithColumnType, + /** Returns number of rows before the current row. */ + val offset: ExpressionWithColumnType = intLiteral(1), + /** Returns value that is used if no row found at such offset. */ + val defaultValue: ExpressionWithColumnType? = null +) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append("LAG(", expr, ", ", offset) + if (defaultValue != null) append(", ", defaultValue) + append(")") + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(expr.columnType, this) + } +} + +/** + * Represents an SQL function that returns value evaluated at the row that is [offset] rows after the current row + * within the partition; if there is no such row, instead returns [defaultValue]. + */ +class Lead( + /** Returns the expression from which the rows are counted. */ + val expr: ExpressionWithColumnType, + /** Returns number of rows before the current row. */ + val offset: ExpressionWithColumnType = intLiteral(1), + /** Returns value that is used if no row found at such offset. */ + val defaultValue: ExpressionWithColumnType? = null +) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append("LEAD(", expr, ", ", offset) + if (defaultValue != null) append(", ", defaultValue) + append(")") + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(expr.columnType, this) + } +} + +/** Represents an SQL function that returns [expr] evaluated at the row that is the first row of the window frame. */ +class FirstValue( + /** Returns the expression to evaluate. */ + val expr: ExpressionWithColumnType +) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append("FIRST_VALUE(", expr, ")") + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(expr.columnType, this) + } +} + +/** Represents an SQL function that returns [expr] evaluated at the row that is the last row of the window frame. */ +class LastValue( + /** Returns the expression to evaluate. */ + val expr: ExpressionWithColumnType +) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append("LAST_VALUE(", expr, ")") + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(expr.columnType, this) + } +} + +/** + * Represents an SQL function that returns [expr] evaluated at the row that is the [n]'th row of the window frame + * (counting from 1); null if no such row + */ +class NthValue( + /** Returns the expression to evaluate. */ + val expr: ExpressionWithColumnType, + /** Returns the row n to find. */ + val n: ExpressionWithColumnType +) : WindowFunction { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { + append("NTH_VALUE(", expr, ", ", n, ")") + } + + override fun over(): WindowFunctionDefinition { + return WindowFunctionDefinition(expr.columnType, this) + } +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/functions/math/MathFunctions.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/functions/math/MathFunctions.kt index f2f42f8a0b..11c2ccff0e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/functions/math/MathFunctions.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/functions/math/MathFunctions.kt @@ -22,7 +22,14 @@ class AbsFunction(expression: ExpressionWithColumnType) : Custom * Returns the smallest integer value that is >= a number */ class CeilingFunction(expression: ExpressionWithColumnType) : CustomFunction( - functionName = if (currentDialectIfAvailable is SQLiteDialect || currentDialectIfAvailable is OracleDialect || currentDialectIfAvailable?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) "CEIL" else "CEILING", + functionName = if ( + currentDialectIfAvailable is SQLiteDialect || currentDialectIfAvailable is OracleDialect || + currentDialectIfAvailable?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle + ) { + "CEIL" + } else { + "CEILING" + }, columnType = LongColumnType(), expr = arrayOf(expression) ) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt index e0f34017b8..0e533c02d2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt @@ -18,7 +18,7 @@ abstract class BaseBatchInsertStatement( internal val data = ArrayList, Any?>>() - private fun Column<*>.isDefaultable() = columnType.nullable || defaultValueFun != null + private fun Column<*>.isDefaultable() = columnType.nullable || defaultValueFun != null || isDatabaseGenerated override operator fun set(column: Column, value: S) { if (data.size > 1 && column !in data[data.size - 2] && !column.isDefaultable()) { @@ -88,7 +88,10 @@ abstract class BaseBatchInsertStatement( override fun valuesAndDefaults(values: Map, Any?>) = arguments!!.first().toMap() override fun prepared(transaction: Transaction, sql: String): PreparedStatementApi { - return if (!shouldReturnGeneratedValues) transaction.connection.prepareStatement(sql, false) - else super.prepared(transaction, sql) + return if (!shouldReturnGeneratedValues) { + transaction.connection.prepareStatement(sql, false) + } else { + super.prepared(transaction, sql) + } } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchInsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchInsertStatement.kt index 53b273d60f..e56170317e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchInsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchInsertStatement.kt @@ -27,15 +27,16 @@ open class SQLServerBatchInsertStatement(table: Table, ignore: Boolean = false, private val columnToReturnValue = table.autoIncColumn?.takeIf { shouldReturnGeneratedValues && it.autoIncColumnType?.nextValExpression == null } - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { val values = arguments!! - val sql = if (values.isEmpty()) "" - else { + val sql = if (values.isEmpty()) { + "" + } else { val output = columnToReturnValue?.let { " OUTPUT inserted.${transaction.identity(it)} AS GENERATED_KEYS" }.orEmpty() - QueryBuilder(true).apply { + QueryBuilder(prepared).apply { values.appendTo(prefix = "$output VALUES") { it.appendTo(prefix = "(", postfix = ")") { (col, value) -> registerArgument(col, value) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchReplaceStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchReplaceStatement.kt index 31d2475c57..18ff0a3860 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchReplaceStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchReplaceStatement.kt @@ -18,14 +18,14 @@ open class BatchReplaceStatement( table: Table, shouldReturnGeneratedValues: Boolean = true ) : BaseBatchInsertStatement(table, ignore = false, shouldReturnGeneratedValues) { - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { val values = arguments!!.first() - val valuesSql = values.toSqlString() + val valuesSql = values.toSqlString(prepared) val dialect = transaction.db.dialect val functionProvider = when (dialect.h2Mode) { H2Dialect.H2CompatibilityMode.MySQL, H2Dialect.H2CompatibilityMode.MariaDB -> MysqlFunctionProvider() else -> dialect.functionProvider } - return functionProvider.replace(table, values.unzip().first, valuesSql, transaction) + return functionProvider.replace(table, values.unzip().first, valuesSql, transaction, prepared) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt index aafa4dcba8..adb43502c8 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt @@ -35,8 +35,8 @@ open class BatchUpdateStatement(val table: IdTable<*>) : UpdateStatement(table, override fun update(column: Column, value: Expression) = error("Expressions unsupported in batch update") - override fun prepareSQL(transaction: Transaction): String = - "${super.prepareSQL(transaction)} WHERE ${transaction.identity(table.id)} = ?" + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = + "${super.prepareSQL(transaction, prepared)} WHERE ${transaction.identity(table.id)} = ?" override fun PreparedStatementApi.executeInternal(transaction: Transaction): Int = if (data.size == 1) executeUpdate() else executeBatch().sum() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt index 2a4ca487aa..c05ac09ff1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpsertStatement.kt @@ -29,7 +29,7 @@ open class BatchUpsertStatement( shouldReturnGeneratedValues: Boolean = true ) : BaseBatchInsertStatement(table, ignore = false, shouldReturnGeneratedValues) { - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { val functionProvider = when (val dialect = transaction.db.dialect) { is H2Dialect -> when (dialect.h2Mode) { H2Dialect.H2CompatibilityMode.MariaDB, H2Dialect.H2CompatibilityMode.MySQL -> MysqlFunctionProvider() @@ -37,6 +37,6 @@ open class BatchUpsertStatement( } else -> dialect.functionProvider } - return functionProvider.upsert(table, arguments!!.first(), onUpdate, null, transaction, *keys) + return functionProvider.upsert(table, arguments!!.first(), onUpdate, null, transaction, keys = keys) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/DeleteStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/DeleteStatement.kt index 714ceab374..7babea8a0d 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/DeleteStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/DeleteStatement.kt @@ -19,8 +19,8 @@ open class DeleteStatement( return executeUpdate() } - override fun prepareSQL(transaction: Transaction): String = - transaction.db.dialect.functionProvider.delete(isIgnore, table, where?.let { QueryBuilder(true).append(it).toString() }, limit, transaction) + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = + transaction.db.dialect.functionProvider.delete(isIgnore, table, where?.let { QueryBuilder(prepared).append(it).toString() }, limit, transaction) override fun arguments(): Iterable>> = QueryBuilder(true).run { where?.toQueryBuilder(this) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertSelectStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertSelectStatement.kt index 8a2d8f855d..1024e827b3 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertSelectStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertSelectStatement.kt @@ -20,6 +20,6 @@ open class InsertSelectStatement(val columns: List>, val selectQuery: override fun arguments(): Iterable>> = selectQuery.arguments() - override fun prepareSQL(transaction: Transaction): String = - transaction.db.dialect.functionProvider.insert(isIgnore, targets.single(), columns, selectQuery.prepareSQL(transaction), transaction) + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = + transaction.db.dialect.functionProvider.insert(isIgnore, targets.single(), columns, selectQuery.prepareSQL(transaction, prepared), transaction) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt index a59aed11cd..7bc04e9d23 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt @@ -2,8 +2,7 @@ package org.jetbrains.exposed.sql.statements import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi -import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect -import org.jetbrains.exposed.sql.vendors.currentDialect +import org.jetbrains.exposed.sql.vendors.* import org.jetbrains.exposed.sql.vendors.inProperCase import java.sql.ResultSet import java.sql.SQLException @@ -11,6 +10,15 @@ import kotlin.properties.Delegates open class InsertStatement(val table: Table, val isIgnore: Boolean = false) : UpdateBuilder(StatementType.INSERT, listOf(table)) { + /** + * Returns the number of rows affected by the insert operation. + * + * When returned by a `BatchInsertStatement` or `BatchUpsertStatement`, the returned value is calculated using the + * sum of the individual values generated by each statement. + * + * **Note**: Some vendors support returning the affected-row value of 2 if an existing row is updated by an upsert + * operation; please check the documentation. + */ var insertedCount: Int by Delegates.notNull() var resultedValues: List? = null @@ -28,7 +36,7 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = fun getOrNull(column: Column): T? = resultedValues?.firstOrNull()?.getOrNull(column) - @Suppress("NestedBlockDepth") + @Suppress("NestedBlockDepth", "ComplexMethod") private fun processResults(rs: ResultSet?, inserted: Int): List { val autoGeneratedKeys = arrayListOf, Any?>>() @@ -62,12 +70,12 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = } } - /** TODO: https://github.com/JetBrains/Exposed/issues/129 - * doesn't work with MySQL `INSERT ... ON DUPLICATE UPDATE` - */ -// assert(isIgnore || autoGeneratedKeys.isEmpty() || autoGeneratedKeys.size == inserted) { -// "Number of autoincs (${autoGeneratedKeys.size}) doesn't match number of batch entries ($inserted)" -// } + assert( + isIgnore || autoGeneratedKeys.isEmpty() || autoGeneratedKeys.size == inserted || + currentDialect.supportsTernaryAffectedRowValues + ) { + "Number of autoincs (${autoGeneratedKeys.size}) doesn't match number of batch entries ($inserted)" + } } } @@ -77,8 +85,11 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = } pairs.forEach { (col, value) -> if (value != DefaultValueMarker) { - if (col.columnType.isAutoInc || value is NextVal<*>) map.getOrPut(col) { value } - else map[col] = value + if (col.columnType.isAutoInc || value is NextVal<*>) { + map.getOrPut(col) { value } + } else { + map[col] = value + } } } @@ -90,6 +101,7 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = return autoGeneratedKeys.map { ResultRow.createAndFillValues(it as Map, Any?>) } } + @Suppress("NestedBlockDepth") protected open fun valuesAndDefaults(values: Map, Any?> = this.values): Map, Any?> { val result = values.toMutableMap() targets.forEach { table -> @@ -106,27 +118,34 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = return result } - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { val values = arguments!!.first() - val sql = values.toSqlString() - return transaction.db.dialect.functionProvider.insert(isIgnore, table, values.map { it.first }, sql, transaction) + val sql = values.toSqlString(prepared) + return transaction.db.dialect.functionProvider + .insert(isIgnore, table, values.map { it.first }, sql, transaction) } - protected fun List, Any?>>.toSqlString(): String { - val builder = QueryBuilder(true) - return if (isEmpty()) "" else with(builder) { - this@toSqlString.appendTo(prefix = "VALUES (", postfix = ")") { (column, value) -> - registerArgument(column, value) + protected fun List, Any?>>.toSqlString(prepared: Boolean): String { + val builder = QueryBuilder(prepared) + return if (isEmpty()) { + "" + } else { + with(builder) { + this@toSqlString.appendTo(prefix = "VALUES (", postfix = ")") { (column, value) -> + registerArgument(column, value) + } + toString() } - toString() } } protected open fun PreparedStatementApi.execInsertFunction(): Pair { - val inserted = if (arguments().count() > 1 || isAlwaysBatch) executeBatch().count() else executeUpdate() + val inserted = if (arguments().count() > 1 || isAlwaysBatch) executeBatch().sum() else executeUpdate() val rs = if (autoIncColumns.isNotEmpty()) { resultSet - } else null + } else { + null + } return inserted to rs } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReplaceStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReplaceStatement.kt index 5a213dfa5e..7bf243e2a2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReplaceStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReplaceStatement.kt @@ -13,14 +13,14 @@ import org.jetbrains.exposed.sql.vendors.h2Mode * @param table Table to either insert values into or delete values from then insert into. */ open class ReplaceStatement(table: Table) : InsertStatement(table) { - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { val values = arguments!!.first() - val valuesSql = values.toSqlString() + val valuesSql = values.toSqlString(prepared) val dialect = transaction.db.dialect val functionProvider = when (dialect.h2Mode) { H2Dialect.H2CompatibilityMode.MySQL, H2Dialect.H2CompatibilityMode.MariaDB -> MysqlFunctionProvider() else -> dialect.functionProvider } - return functionProvider.replace(table, values.unzip().first, valuesSql, transaction) + return functionProvider.replace(table, values.unzip().first, valuesSql, transaction, prepared) } } 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 191b9097f4..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 @@ -16,7 +16,7 @@ abstract class Statement(val type: StatementType, val targets: List
>> @@ -58,10 +58,11 @@ abstract class Statement(val type: StatementType, val targets: List
, executedStatement: PreparedStatementApi) {} fun beforeCommit(transaction: Transaction) {} - @Deprecated("using afterCommit with transaction", level = DeprecationLevel.ERROR) - // @Deprecated("using afterCommit with transaction", level = DeprecationLevel.HIDDEN) \\ next version, backward compatibility - fun afterCommit() {} fun afterCommit(transaction: Transaction) {} fun beforeRollback(transaction: Transaction) {} - @Deprecated("using afterRollback with transaction", level = DeprecationLevel.ERROR) - // @Deprecated("using afterRollback with transaction", level = DeprecationLevel.HIDDEN) \\ next version, backward compatibility - fun afterRollback() {} fun afterRollback(transaction: Transaction) {} fun keepUserDataInTransactionStoreOnCommit(userData: Map, Any?>): Map, Any?> = emptyMap() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateStatement.kt index 3832136d8b..0e4c7254ae 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateStatement.kt @@ -5,6 +5,8 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi import org.jetbrains.exposed.sql.vendors.H2Dialect.H2CompatibilityMode import org.jetbrains.exposed.sql.vendors.H2FunctionProvider +import org.jetbrains.exposed.sql.vendors.OracleDialect +import org.jetbrains.exposed.sql.vendors.currentDialect import org.jetbrains.exposed.sql.vendors.h2Mode open class UpdateStatement(val targetsSet: ColumnSet, val limit: Int?, val where: Op? = null) : @@ -17,7 +19,7 @@ open class UpdateStatement(val targetsSet: ColumnSet, val limit: Int?, val where return executeUpdate() } - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { require(firstDataSet.isNotEmpty()) { "Can't prepare UPDATE statement without fields to update" } val dialect = transaction.db.dialect @@ -35,10 +37,17 @@ open class UpdateStatement(val targetsSet: ColumnSet, val limit: Int?, val where } override fun arguments(): Iterable>> = QueryBuilder(true).run { - values.forEach { - registerArgument(it.key, it.value) + if (targetsSet is Join && currentDialect is OracleDialect) { + where?.toQueryBuilder(this) + values.forEach { + registerArgument(it.key, it.value) + } + } else { + values.forEach { + registerArgument(it.key, it.value) + } + where?.toQueryBuilder(this) } - where?.toQueryBuilder(this) if (args.isNotEmpty()) listOf(args) else emptyList() } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt index 39fb9191fb..2294dedf1e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpsertStatement.kt @@ -20,7 +20,7 @@ open class UpsertStatement( val where: Op? ) : InsertStatement(table) { - override fun prepareSQL(transaction: Transaction): String { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { val functionProvider = when (val dialect = transaction.db.dialect) { is H2Dialect -> when (dialect.h2Mode) { H2Dialect.H2CompatibilityMode.MariaDB, H2Dialect.H2CompatibilityMode.MySQL -> MysqlFunctionProvider() @@ -28,6 +28,6 @@ open class UpsertStatement( } else -> dialect.functionProvider } - return functionProvider.upsert(table, arguments!!.first(), onUpdate, where, transaction, *keys) + return functionProvider.upsert(table, arguments!!.first(), onUpdate, where, transaction, keys = keys) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ConnectionApi.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ConnectionApi.kt index 8672768812..87ef38be7a 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ConnectionApi.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ConnectionApi.kt @@ -1,4 +1,5 @@ @file:Suppress("Filename", "MatchingDeclarationName") + package org.jetbrains.exposed.sql.statements.api interface ExposedConnection { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedBlob.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedBlob.kt index 0317163b9f..1b05939d60 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedBlob.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedBlob.kt @@ -1,5 +1,8 @@ package org.jetbrains.exposed.sql.statements.api +import org.jetbrains.exposed.sql.vendors.OracleDialect +import org.jetbrains.exposed.sql.vendors.currentDialectIfAvailable +import java.io.IOException import java.io.InputStream class ExposedBlob(inputStream: InputStream) { @@ -8,12 +11,20 @@ class ExposedBlob(inputStream: InputStream) { var inputStream = inputStream private set - val bytes get() = inputStream.readBytes().also { - if (inputStream.markSupported()) - inputStream.reset() - else - inputStream = it.inputStream() - } + val bytes: ByteArray + get() = inputStream.readBytes().also { + if (inputStream.markSupported()) { + try { + inputStream.reset() + } catch (_: IOException) { + if (currentDialectIfAvailable is OracleDialect) { + inputStream = it.inputStream() + } + } + } else { + inputStream = it.inputStream() + } + } override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata.kt index 572018165c..19e6225062 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata.kt @@ -4,6 +4,7 @@ import org.jetbrains.exposed.sql.ForeignKeyConstraint import org.jetbrains.exposed.sql.Index import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.vendors.ColumnMetadata +import org.jetbrains.exposed.sql.vendors.PrimaryKeyMetadata import java.math.BigDecimal abstract class ExposedDatabaseMetadata(val database: String) { @@ -33,6 +34,8 @@ abstract class ExposedDatabaseMetadata(val database: String) { abstract fun existingIndices(vararg tables: Table): Map> + abstract fun existingPrimaryKeys(vararg tables: Table): Map + abstract fun tableConstraints(tables: List
): Map> abstract fun cleanCache() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/IdentifierManagerApi.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/IdentifierManagerApi.kt index 06e570a94f..a2b9c7391e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/IdentifierManagerApi.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/IdentifierManagerApi.kt @@ -1,5 +1,6 @@ package org.jetbrains.exposed.sql.statements.api +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.ANSI_SQL_2003_KEYWORDS import org.jetbrains.exposed.sql.vendors.VENDORS_KEYWORDS import org.jetbrains.exposed.sql.vendors.currentDialect @@ -30,6 +31,7 @@ abstract class IdentifierManagerApi { } private val checkedIdentitiesCache = IdentifiersCache() + private val checkedKeywordsCache = IdentifiersCache() private val shouldQuoteIdentifiersCache = IdentifiersCache() private val identifiersInProperCaseCache = IdentifiersCache() private val quotedIdentifiersCache = IdentifiersCache() @@ -37,9 +39,27 @@ abstract class IdentifierManagerApi { private fun String.isIdentifier() = !isEmpty() && first().isIdentifierStart() && all { it.isIdentifierStart() || it in '0'..'9' } private fun Char.isIdentifierStart(): Boolean = this in 'a'..'z' || this in 'A'..'Z' || this == '_' || this in extraNameCharacters + private fun String.isAKeyword(): Boolean = checkedKeywordsCache.getOrPut(lowercase()) { + keywords.any { this.equals(it, true) } + } + + @Deprecated( + message = "This will be removed in future releases when the behavior becomes default in IdentifierManagerApi", + level = DeprecationLevel.WARNING + ) + internal fun isUnflaggedKeyword(identity: String): Boolean = identity.isAKeyword() && !shouldPreserveKeywordCasing + + @Deprecated( + message = "This will be removed in future releases when the behavior becomes default in IdentifierManagerApi", + level = DeprecationLevel.WARNING + ) + private val shouldPreserveKeywordCasing by lazy { + TransactionManager.currentOrNull()?.db?.config?.preserveKeywordCasing == true + } + fun needQuotes(identity: String): Boolean { return checkedIdentitiesCache.getOrPut(identity.lowercase()) { - !identity.isAlreadyQuoted() && (keywords.any { identity.equals(it, true) } || !identity.isIdentifier()) + !identity.isAlreadyQuoted() && (identity.isAKeyword() || !identity.isIdentifier()) } } @@ -51,6 +71,7 @@ abstract class IdentifierManagerApi { val alreadyUpper = identity == identity.uppercase() when { alreadyQuoted -> false + identity.isAKeyword() && shouldPreserveKeywordCasing -> true supportsMixedIdentifiers -> false alreadyLower && isLowerCaseIdentifiers -> false alreadyUpper && isUpperCaseIdentifiers -> false @@ -67,6 +88,7 @@ abstract class IdentifierManagerApi { alreadyQuoted && isUpperCaseQuotedIdentifiers -> identity.uppercase() alreadyQuoted && isLowerCaseQuotedIdentifiers -> identity.lowercase() supportsMixedIdentifiers -> identity + identity.isAKeyword() && shouldPreserveKeywordCasing -> identity oracleVersion != OracleVersion.NonOracle -> identity.uppercase() isUpperCaseIdentifiers -> identity.uppercase() isLowerCaseIdentifiers -> identity.lowercase() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManager.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManager.kt index 3745f69e80..67bed2d1ad 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManager.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManager.kt @@ -10,6 +10,7 @@ import org.jetbrains.exposed.sql.exposedLogger import org.jetbrains.exposed.sql.statements.api.ExposedConnection import org.jetbrains.exposed.sql.statements.api.ExposedSavepoint import java.sql.SQLException +import java.util.concurrent.ThreadLocalRandom class ThreadLocalTransactionManager( private val db: Database, @@ -21,6 +22,18 @@ class ThreadLocalTransactionManager( @TestOnly set + @Volatile + override var defaultMinRepetitionDelay: Long = db.config.defaultMinRepetitionDelay + @Deprecated("Use DatabaseConfig to define the defaultMinRepetitionDelay") + @TestOnly + set + + @Volatile + override var defaultMaxRepetitionDelay: Long = db.config.defaultMaxRepetitionDelay + @Deprecated("Use DatabaseConfig to define the defaultMaxRepetitionDelay") + @TestOnly + set + @Volatile override var defaultIsolationLevel: Int = db.config.defaultIsolationLevel get() { @@ -29,6 +42,7 @@ class ThreadLocalTransactionManager( } return field } + @Deprecated("Use DatabaseConfig to define the defaultIsolationLevel") @TestOnly set @@ -38,6 +52,10 @@ class ThreadLocalTransactionManager( val threadLocal = ThreadLocal() + 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( @@ -89,9 +107,7 @@ class ThreadLocalTransactionManager( get() = connectionLazy.value private val useSavePoints = outerTransaction != null && db.useNestedTransactions - private var savepoint: ExposedSavepoint? = if (useSavePoints) { - connection.setSavepoint(savepointName) - } else null + private var savepoint: ExposedSavepoint? = if (useSavePoints) connection.setSavepoint(savepointName) else null override fun commit() { if (connectionLazy.isInitialized()) { @@ -145,66 +161,71 @@ class ThreadLocalTransactionManager( fun transaction(db: Database? = null, statement: Transaction.() -> T): T = transaction( db.transactionManager.defaultIsolationLevel, - db.transactionManager.defaultRepetitionAttempts, db.transactionManager.defaultReadOnly, - db, statement + db, + statement ) fun transaction( transactionIsolation: Int, - repetitionAttempts: Int, 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, repetitionAttempts, readOnly, db, null, statement) - } + } ?: inTopLevelTransaction( + transactionIsolation, + readOnly, + db, + null, + statement + ) } +} fun inTopLevelTransaction( transactionIsolation: Int, - repetitionAttempts: Int, readOnly: Boolean = false, db: Database? = null, outerTransaction: Transaction? = null, statement: Transaction.() -> T ): T { - fun run(): T { var repetitions = 0 val outerManager = outerTransaction?.db.transactionManager.takeIf { it.currentOrNull() != null } + var intermediateDelay: Long = 0 + var retryInterval: Long? = null + while (true) { db?.let { db.transactionManager.let { m -> TransactionManager.resetCurrent(m) } } val transaction = db.transactionManager.newTransaction(transactionIsolation, readOnly, outerTransaction) @@ -215,13 +236,34 @@ fun inTopLevelTransaction( val answer = transaction.statement() transaction.commit() return answer - } catch (e: SQLException) { - handleSQLException(e, transaction, repetitions) + } catch (cause: SQLException) { + handleSQLException(cause, transaction, repetitions) repetitions++ - if (repetitions >= repetitionAttempts) { - throw e + if (repetitions >= transaction.repetitionAttempts) { + throw cause + } + + if (retryInterval == null) { + retryInterval = transaction.getRetryInterval() + intermediateDelay = transaction.minRepetitionDelay + } + // set delay value with an exponential backoff time period. + val delay = when { + transaction.minRepetitionDelay < transaction.maxRepetitionDelay -> { + intermediateDelay += retryInterval * repetitions + ThreadLocalRandom.current().nextLong(intermediateDelay, intermediateDelay + retryInterval) + } + + transaction.minRepetitionDelay == transaction.maxRepetitionDelay -> transaction.minRepetitionDelay + else -> 0 } - } catch (e: Throwable) { + exposedLogger.warn("Wait $delay milliseconds before retrying") + try { + Thread.sleep(delay) + } catch (cause: InterruptedException) { + // Do nothing + } + } catch (cause: Throwable) { val currentStatement = transaction.currentStatement transaction.rollbackLoggingException { exposedLogger.warn( @@ -229,7 +271,7 @@ fun inTopLevelTransaction( it ) } - throw e + throw cause } finally { TransactionManager.resetCurrent(outerManager) closeStatementsAndConnection(transaction) @@ -252,17 +294,19 @@ private fun keepAndRestoreTransactionRefAfterRun(db: Database? = null, block } } -internal fun handleSQLException(e: SQLException, transaction: Transaction, repetitions: Int) { - val exposedSQLException = e as? ExposedSQLException +internal fun handleSQLException(cause: SQLException, transaction: Transaction, repetitions: Int) { + val exposedSQLException = cause as? ExposedSQLException val queriesToLog = exposedSQLException?.causedByQueries()?.joinToString(";\n") ?: "${transaction.currentStatement}" - val message = "Transaction attempt #$repetitions failed: ${e.message}. Statement(s): $queriesToLog" + val message = "Transaction attempt #$repetitions failed: ${cause.message}. Statement(s): $queriesToLog" exposedSQLException?.contexts?.forEach { transaction.interceptors.filterIsInstance().forEach { logger -> logger.log(it, transaction) } } - exposedLogger.warn(message, e) - transaction.rollbackLoggingException { exposedLogger.warn("Transaction rollback failed: ${it.message}. See previous log line for statement", it) } + exposedLogger.warn(message, cause) + transaction.rollbackLoggingException { + exposedLogger.warn("Transaction rollback failed: ${it.message}. See previous log line for statement", it) + } } internal fun closeStatementsAndConnection(transaction: Transaction) { @@ -274,8 +318,10 @@ internal fun closeStatementsAndConnection(transaction: Transaction) { transaction.currentStatement = null } transaction.closeExecutedStatements() - } catch (e: Exception) { - exposedLogger.warn("Statements close failed", e) + } catch (cause: Exception) { + exposedLogger.warn("Statements close failed", cause) + } + transaction.closeLoggingException { + exposedLogger.warn("Transaction close failed: ${it.message}. Statement: $currentStatement", it) } - transaction.closeLoggingException { exposedLogger.warn("Transaction close failed: ${it.message}. Statement: $currentStatement", it) } } 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 ad8b207329..116c78327b 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 @@ -33,6 +33,10 @@ private object NotInitializedManager : TransactionManager { override var defaultRepetitionAttempts: Int = -1 + override var defaultMinRepetitionDelay: Long = 0 + + override var defaultMaxRepetitionDelay: Long = 0 + override fun newTransaction(isolation: Int, readOnly: Boolean, outerTransaction: Transaction?): Transaction = error("Please call Database.connect() before using this code") @@ -51,6 +55,10 @@ interface TransactionManager { var defaultRepetitionAttempts: Int + var defaultMinRepetitionDelay: Long + + var defaultMaxRepetitionDelay: Long + fun newTransaction( isolation: Int = defaultIsolationLevel, readOnly: Boolean = defaultReadOnly, @@ -64,9 +72,12 @@ interface TransactionManager { companion object { internal val currentDefaultDatabase = AtomicReference() + @Suppress("SpacingBetweenDeclarationsWithAnnotations") var defaultDatabase: Database? @Synchronized get() = currentDefaultDatabase.get() ?: databases.firstOrNull() - @Synchronized set(value) { currentDefaultDatabase.set(value) } + @Synchronized set(value) { + currentDefaultDatabase.set(value) + } private val databases = ConcurrentLinkedDeque() @@ -100,6 +111,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 @@ -153,5 +168,7 @@ internal inline fun TransactionInterface.closeLoggingException(log: (Exception) } } +@Suppress("TooGenericExceptionThrown") val Database?.transactionManager: TransactionManager - get() = TransactionManager.managerFor(this) ?: throw RuntimeException("database $this don't have any transaction manager") + get() = TransactionManager.managerFor(this) + ?: throw RuntimeException("database $this don't have any transaction manager") diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionScope.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionScope.kt index aabfbbb94d..2eaf231097 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionScope.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/TransactionScope.kt @@ -13,7 +13,6 @@ class TransactionStore(val init: (Transaction.() -> T)? = null) : ReadW private val key = Key() - @Suppress("UNCHECKED_CAST") override fun getValue(thisRef: Any?, property: KProperty<*>): T? { val currentOrNullTransaction = TransactionManager.currentOrNull() return currentOrNullTransaction?.getUserData(key) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/experimental/Suspended.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/experimental/Suspended.kt index cbfbe7c2b7..c52b09f7f4 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/experimental/Suspended.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/transactions/experimental/Suspended.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.ThreadContextElement import kotlinx.coroutines.async +import kotlinx.coroutines.delay import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.exposedLogger @@ -13,17 +14,22 @@ import org.jetbrains.exposed.sql.transactions.handleSQLException import org.jetbrains.exposed.sql.transactions.rollbackLoggingException import org.jetbrains.exposed.sql.transactions.transactionManager import java.sql.SQLException +import java.util.concurrent.ThreadLocalRandom import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext internal class TransactionContext(val manager: TransactionManager?, val transaction: Transaction?) -internal class TransactionScope(internal val tx: Lazy, parent: CoroutineContext) : CoroutineScope, CoroutineContext.Element { +internal class TransactionScope( + internal val tx: Lazy, + parent: CoroutineContext +) : CoroutineScope, CoroutineContext.Element { private val baseScope = CoroutineScope(parent) override val coroutineContext get() = baseScope.coroutineContext + this override val key = Companion - internal fun holdsSameTransaction(transaction: Transaction?) = transaction != null && tx.isInitialized() && tx.value == transaction + internal fun holdsSameTransaction(transaction: Transaction?) = + transaction != null && tx.isInitialized() && tx.value == transaction companion object : CoroutineContext.Key } @@ -49,6 +55,12 @@ internal class TransactionCoroutineElement( companion object : CoroutineContext.Key } +/** + * Creates a new `TransactionScope` then calls the specified suspending [statement], suspends until it completes, and returns the result. + * + * The `TransactionScope` is derived from a new `Transaction` and a given coroutine [context], + * or the current `coroutineContext` if no [context] is provided. + */ suspend fun newSuspendedTransaction( context: CoroutineContext? = null, db: Database? = null, @@ -59,11 +71,26 @@ suspend fun newSuspendedTransaction( suspendedTransactionAsyncInternal(true, statement).await() } -suspend fun Transaction.suspendedTransaction(context: CoroutineContext? = null, statement: suspend Transaction.() -> T): T = +/** + * Calls the specified suspending [statement], suspends until it completes, and returns the result. + * + * The resulting `TransactionScope` is derived from the current `coroutineContext` if the latter already holds [this] `Transaction`; + * otherwise, a new scope is created using [this] `Transaction` and a given coroutine [context]. + */ +suspend fun Transaction.withSuspendTransaction( + context: CoroutineContext? = null, + statement: suspend Transaction.() -> T +): T = withTransactionScope(context, this, db = null, transactionIsolation = null) { suspendedTransactionAsyncInternal(false, statement).await() } +/** + * Creates a new `TransactionScope` and returns its future result as an implementation of `Deferred`. + * + * The `TransactionScope` is derived from a new `Transaction` and a given coroutine [context], + * or the current `coroutineContext` if no [context] is provided. + */ suspend fun suspendedTransactionAsync( context: CoroutineContext? = null, db: Database? = null, @@ -98,10 +125,13 @@ private suspend fun withTransactionScope( body: suspend TransactionScope.() -> T ): T { val currentScope = coroutineContext[TransactionScope] - suspend fun newScope(_tx: Transaction?): T { - val manager = (_tx?.db ?: db ?: TransactionManager.currentDefaultDatabase.get())?.transactionManager ?: TransactionManager.manager + suspend fun newScope(currentTransaction: Transaction?): T { + val currentDatabase: Database? = currentTransaction?.db ?: db ?: TransactionManager.currentDefaultDatabase.get() + val manager = currentDatabase?.transactionManager ?: TransactionManager.manager - val tx = lazy(LazyThreadSafetyMode.NONE) { _tx ?: manager.newTransaction(transactionIsolation ?: manager.defaultIsolationLevel) } + val tx = lazy(LazyThreadSafetyMode.NONE) { + currentTransaction ?: manager.newTransaction(transactionIsolation ?: manager.defaultIsolationLevel) + } val element = TransactionCoroutineElement(tx, manager) @@ -119,24 +149,73 @@ private suspend fun withTransactionScope( } } +private fun Transaction.resetIfClosed(): Transaction { + return if (connection.isClosed) { + // Repetition attempts will throw org.h2.jdbc.JdbcSQLException: The object is already closed + // unless the transaction is reset before every attempt (after the 1st failed attempt) + val currentManager = db.transactionManager + currentManager.bindTransactionToThread(this) + TransactionManager.resetCurrent(currentManager) + currentManager.newTransaction(transactionIsolation, readOnly, outerTransaction) + } else { + this + } +} + +@Suppress("CyclomaticComplexMethod") private fun TransactionScope.suspendedTransactionAsyncInternal( shouldCommit: Boolean, statement: suspend Transaction.() -> T ): Deferred = async { - val transaction = tx.value - @Suppress("TooGenericExceptionCaught") - try { - transaction.statement().apply { - if (shouldCommit) transaction.commit() + var repetitions = 0 + var intermediateDelay: Long = 0 + var retryInterval: Long? = null + + var answer: T + while (true) { + val transaction = if (repetitions == 0) tx.value else tx.value.resetIfClosed() + + @Suppress("TooGenericExceptionCaught") + try { + answer = transaction.statement().apply { + if (shouldCommit) transaction.commit() + } + break + } catch (cause: SQLException) { + handleSQLException(cause, transaction, repetitions) + repetitions++ + if (repetitions >= transaction.repetitionAttempts) { + throw cause + } + + if (retryInterval == null) { + retryInterval = transaction.getRetryInterval() + intermediateDelay = transaction.minRepetitionDelay + } + // set delay value with an exponential backoff time period + val repetitionDelay = when { + transaction.minRepetitionDelay < transaction.maxRepetitionDelay -> { + intermediateDelay += retryInterval * repetitions + ThreadLocalRandom.current().nextLong(intermediateDelay, intermediateDelay + retryInterval) + } + transaction.minRepetitionDelay == transaction.maxRepetitionDelay -> transaction.minRepetitionDelay + else -> 0 + } + exposedLogger.warn("Wait $repetitionDelay milliseconds before retrying") + try { + delay(repetitionDelay) + } catch (cause: InterruptedException) { + // Do nothing + } + } catch (cause: Throwable) { + val currentStatement = transaction.currentStatement + transaction.rollbackLoggingException { + exposedLogger.warn("Transaction rollback failed: ${it.message}. Statement: $currentStatement", it) + } + throw cause + } finally { + if (shouldCommit) transaction.closeAsync() } - } catch (e: SQLException) { - handleSQLException(e, transaction, 1) - throw e - } catch (e: Throwable) { - val currentStatement = transaction.currentStatement - transaction.rollbackLoggingException { exposedLogger.warn("Transaction rollback failed: ${it.message}. Statement: $currentStatement", it) } - throw e - } finally { - if (shouldCommit) transaction.closeAsync() } + answer } 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..cc8448b733 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt @@ -0,0 +1,186 @@ +package org.jetbrains.exposed.sql.vendors + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.sql.DatabaseMetaData + +/** + * 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 supportsOnUpdate: Boolean get() = true + + val supportsSetDefaultReferenceOption: Boolean get() = true + + val supportsRestrictReferenceOption: Boolean get() = true + + 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") + } + } + + /** Returns the corresponding [ReferenceOption] for the specified [refOption] from JDBC. */ + fun resolveRefOptionFromJdbc(refOption: Int): ReferenceOption = when (refOption) { + DatabaseMetaData.importedKeyCascade -> ReferenceOption.CASCADE + DatabaseMetaData.importedKeySetNull -> ReferenceOption.SET_NULL + DatabaseMetaData.importedKeyRestrict -> ReferenceOption.RESTRICT + DatabaseMetaData.importedKeyNoAction -> ReferenceOption.NO_ACTION + DatabaseMetaData.importedKeySetDefault -> ReferenceOption.SET_DEFAULT + else -> currentDialect.defaultReferenceOption + } + + 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 53% 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 1c856a27bb..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,128 +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. */ - open fun ubyteType(): String = "TINYINT" - - /** Numeric type for storing 2-byte integers. */ - open fun shortType(): String = "SMALLINT" - - /** Numeric type for storing 2-byte unsigned integers. */ - open fun ushortType(): String = "SMALLINT" - - /** Numeric type for storing 4-byte integers. */ - open fun integerType(): String = "INT" - - /** Numeric type for storing 4-byte unsigned integers. */ - open fun uintegerType(): String = "INT" - - /** 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)" - - 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" - - /** 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() - - // 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" - 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. @@ -260,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) } @@ -429,6 +309,74 @@ abstract class FunctionProvider { append("VAR_SAMP(", expression, ")") } + // JSON Functions + + /** + * SQL function that extracts data from a JSON object at the specified [path], either as a JSON representation or as a scalar value. + * + * @param expression Expression from which to extract JSON subcomponents matched by [path]. + * @param path String(s) representing JSON path/keys that match fields to be extracted. + * **Note:** Multiple [path] arguments are not supported by all vendors; please check the documentation. + * @param toScalar If `true`, the extracted result is a scalar or text value; otherwise, it is a JSON object. + * @param jsonType Column type of [expression] to check, if casting to JSONB is required. + * @param queryBuilder Query builder to append the SQL function to. + */ + open fun jsonExtract( + expression: Expression, + vararg path: String, + toScalar: Boolean, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + throw UnsupportedByDialectException( + "There's no generic SQL for JSON_EXTRACT. There must be a vendor specific implementation", currentDialect + ) + } + + /** + * SQL function that checks whether a [candidate] expression is contained within a JSON [target]. + * + * @param target JSON expression being searched. + * @param candidate Expression to search for in [target]. + * @param path String representing JSON path/keys that match specific fields to search for [candidate]. + * **Note:** A [path] argument is not supported by all vendors; please check the documentation. + * @param jsonType Column type of [target] to check, if casting to JSONB is required. + * @param queryBuilder Query builder to append the SQL function to. + */ + open fun jsonContains( + target: Expression<*>, + candidate: Expression<*>, + path: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + throw UnsupportedByDialectException( + "There's no generic SQL for JSON_CONTAINS. There must be a vendor specific implementation", currentDialect + ) + } + + /** + * SQL function that checks whether data exists within a JSON [expression] at the specified [path]. + * + * @param expression JSON expression being checked. + * @param path String(s) representing JSON path/keys that match fields to check for existing data. + * **Note:** Multiple [path] arguments are not supported by all vendors; please check the documentation. + * @param optional String representing any optional vendor-specific clause or argument. + * @param jsonType Column type of [expression] to check, if casting to JSONB is required. + * @param queryBuilder Query builder to append the SQL function to. + */ + open fun jsonExists( + expression: Expression<*>, + vararg path: String, + optional: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + throw UnsupportedByDialectException( + "There's no generic SQL for JSON_EXISTS. There must be a vendor specific implementation", currentDialect + ) + } + // Commands @Suppress("VariableNaming") open val DEFAULT_VALUE_EXPRESSION: String = "DEFAULT VALUES" @@ -556,7 +504,8 @@ abstract class FunctionProvider { table: Table, columns: List>, expression: String, - transaction: Transaction + transaction: Transaction, + prepared: Boolean = true ): String = transaction.throwUnsupportedException("There's no generic SQL for REPLACE. There must be a vendor specific implementation.") /** @@ -669,9 +618,9 @@ abstract class FunctionProvider { onUpdate?.appendTo { (columnToUpdate, updateExpression) -> if (isAliasNeeded) { val aliasExpression = updateExpression.toString().replace(transaction.identity(table), "T") - append("T.${transaction.identity(columnToUpdate)}=${aliasExpression}") + append("T.${transaction.identity(columnToUpdate)}=$aliasExpression") } else { - append("${transaction.identity(columnToUpdate)}=${updateExpression}") + append("${transaction.identity(columnToUpdate)}=$updateExpression") } } ?: run { updateColumns.appendTo { column -> @@ -734,382 +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?, -) - -/** - * 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 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 - - 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 `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): String - - /** Returns the SQL command that modifies the specified [column]. */ - fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List - - 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) - - - open class ForNoKeyUpdate(mode: MODE? = null, vararg ofTables: Table) : ForUpdateBase("FOR NO KEY UPDATE", mode, *ofTables) { - companion object : ForNoKeyUpdate() - } - - open class ForShare(mode: MODE? = null, vararg ofTables: Table) : ForUpdateBase("FOR SHARE", mode, *ofTables) { - companion object : ForShare() - } - - open class ForKeyShare(mode: MODE? = null, vararg ofTables: Table) : ForUpdateBase("FOR KEY SHARE", mode, *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 { - - 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.tableName.substringBefore('.', "").takeIf { it.isNotEmpty() } - 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.nameInDatabaseCase() - else -> it == "$scheme.${table.tableNameWithoutScheme}".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.nameInDatabaseCase()) } - - fillConstraintCacheForTables(tablesToLoad) - tables.forEach { table -> - columnConstraintsCache[table.nameInDatabaseCase()].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) } - - 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() - } - - 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 columnsList = index.columns.joinToString(prefix = "(", postfix = ")") { t.identity(it) } - return when { - index.unique -> { - "ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $columnsList" - } - index.indexType != null -> { - createIndexWithType(name = quotedIndexName, table = quotedTableName, columns = columnsList, type = index.indexType) - } - else -> { - "CREATE INDEX $quotedIndexName ON $quotedTableName $columnsList" - } - } - } - - protected open fun createIndexWithType(name: String, table: String, columns: String, type: String): String { - return "CREATE INDEX $name ON $table $columns USING $type" - } - - override fun dropIndex(tableName: String, indexName: String): String { - val identifierManager = TransactionManager.current().db.identifierManager - 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)}") -} - -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 7d710e61c1..d23dfc3ca6 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 @@ -4,6 +4,7 @@ import org.intellij.lang.annotations.Language import org.jetbrains.exposed.exceptions.throwUnsupportedException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.util.* internal object H2DataTypeProvider : DataTypeProvider() { override fun binaryType(): String { @@ -12,7 +13,13 @@ internal object H2DataTypeProvider : DataTypeProvider() { } override fun uuidType(): String = "UUID" + override fun uuidToDB(value: UUID): Any = value.toString() override fun dateTimeType(): String = "DATETIME(9)" + + override fun timestampWithTimeZoneType(): String = "TIMESTAMP(9) WITH TIME ZONE" + + override fun jsonBType(): String = "JSON" + override fun hexToDb(hexString: String): String = "X'$hexString'" } @@ -115,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 } @@ -152,8 +162,8 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function private var delegatedDialect: DatabaseDialect? = null - private fun resolveDelegatedDialect() : DatabaseDialect? { - return delegatedDialect ?: delegatedDialectNameProvider?.dialectName?.let { + private fun resolveDelegatedDialect(): DatabaseDialect? { + return delegatedDialect ?: delegatedDialectNameProvider?.dialectName?.lowercase()?.let { val dialect = Database.dialects[it]?.invoke() ?: error("Can't resolve dialect for $it") delegatedDialect = dialect dialect @@ -177,6 +187,7 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function H2MajorVersion.One -> "NAME" to "VALUE" H2MajorVersion.Two -> "SETTING_NAME" to "SETTING_VALUE" } + @Language("H2") val fetchModeQuery = "SELECT $settingValueField FROM INFORMATION_SCHEMA.SETTINGS WHERE $settingNameField = 'MODE'" val modeValue = TransactionManager.current().exec(fetchModeQuery) { rs -> @@ -207,12 +218,17 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function override val supportsCreateSequence: Boolean by lazy { resolveDelegatedDialect()?.supportsCreateSequence ?: super.supportsCreateSequence } override val needsSequenceToAutoInc: Boolean by lazy { resolveDelegatedDialect()?.needsSequenceToAutoInc ?: super.needsSequenceToAutoInc } override val defaultReferenceOption: ReferenceOption by lazy { resolveDelegatedDialect()?.defaultReferenceOption ?: super.defaultReferenceOption } -// override val needsQuotesWhenSymbolsInNames: Boolean by lazy { resolveDelegatedDialect()?.needsQuotesWhenSymbolsInNames ?: super.needsQuotesWhenSymbolsInNames } - override val supportsSequenceAsGeneratedKeys: Boolean by lazy { resolveDelegatedDialect()?.supportsSequenceAsGeneratedKeys ?: super.supportsSequenceAsGeneratedKeys } + override val supportsSequenceAsGeneratedKeys: Boolean by lazy { + resolveDelegatedDialect()?.supportsSequenceAsGeneratedKeys ?: super.supportsSequenceAsGeneratedKeys + } + override val supportsTernaryAffectedRowValues: Boolean by lazy { + resolveDelegatedDialect()?.supportsTernaryAffectedRowValues ?: super.supportsTernaryAffectedRowValues + } override val supportsCreateSchema: Boolean by lazy { resolveDelegatedDialect()?.supportsCreateSchema ?: super.supportsCreateSchema } override val supportsSubqueryUnions: Boolean by lazy { resolveDelegatedDialect()?.supportsSubqueryUnions ?: super.supportsSubqueryUnions } override val supportsDualTableConcept: Boolean by lazy { resolveDelegatedDialect()?.supportsDualTableConcept ?: super.supportsDualTableConcept } override val supportsOrderByNullsFirstLast: Boolean by lazy { resolveDelegatedDialect()?.supportsOrderByNullsFirstLast ?: super.supportsOrderByNullsFirstLast } + override val supportsWindowFrameGroupsMode: Boolean by lazy { resolveDelegatedDialect()?.supportsWindowFrameGroupsMode ?: super.supportsWindowFrameGroupsMode } // override val likePatternSpecialChars: Map by lazy { resolveDelegatedDialect()?.likePatternSpecialChars ?: super.likePatternSpecialChars } override fun existingIndices(vararg tables: Table): Map> = @@ -235,17 +251,25 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function ) return "" } + if (index.functions != null) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in H2" + ) + return "" + } return super.createIndex(index) } 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") } override fun dropDatabase(name: String) = "DROP SCHEMA IF EXISTS ${name.inProperCase()}" - companion object : DialectNameProvider("h2") + companion object : DialectNameProvider("H2") } val DatabaseDialect.h2Mode: H2Dialect.H2CompatibilityMode? get() = (this as? H2Dialect)?.h2Mode diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt index 9d65c28d7b..74f1eeb434 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt @@ -1,9 +1,11 @@ package org.jetbrains.exposed.sql.vendors import org.jetbrains.exposed.sql.Expression +import org.jetbrains.exposed.sql.Index import org.jetbrains.exposed.sql.QueryBuilder import org.jetbrains.exposed.sql.Sequence import org.jetbrains.exposed.sql.append +import org.jetbrains.exposed.sql.exposedLogger internal object MariaDBFunctionProvider : MysqlFunctionProvider() { override fun nextVal(seq: Sequence, builder: QueryBuilder) = builder { @@ -35,6 +37,17 @@ class MariaDBDialect : MysqlDialect() { override val name: String = dialectName override val functionProvider: FunctionProvider = MariaDBFunctionProvider override val supportsOnlyIdentifiersInGeneratedKeys: Boolean = true + override val supportsSetDefaultReferenceOption: Boolean = false - companion object : DialectNameProvider("mariadb") + override fun createIndex(index: Index): String { + if (index.functions != null) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in MariaDB" + ) + return "" + } + return super.createIndex(index) + } + + companion object : DialectNameProvider("MariaDB") } 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 71% 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 9b86b5620d..c1d1b13c89 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 @@ -15,6 +15,17 @@ internal object MysqlDataTypeProvider : DataTypeProvider() { override fun dateTimeType(): String = if ((currentDialect as? MysqlDialect)?.isFractionDateTimeSupported() == true) "DATETIME(6)" else "DATETIME" + override fun timestampWithTimeZoneType(): String = + if ((currentDialect as? MysqlDialect)?.isTimeZoneOffsetSupported() == true) { + "TIMESTAMP(6)" + } else { + throw UnsupportedByDialectException( + "This vendor does not support timestamp with time zone data type" + + ((currentDialect as? MariaDBDialect)?.let { "" } ?: " for this version"), + currentDialect + ) + } + override fun ubyteType(): String = "TINYINT UNSIGNED" override fun ushortType(): String = "SMALLINT UNSIGNED" @@ -37,8 +48,22 @@ internal object MysqlDataTypeProvider : DataTypeProvider() { else -> value.toBoolean() } - override fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) { + override fun jsonBType(): String = "JSON" + + override fun processForDefaultValue(e: Expression<*>): String = when { + e is LiteralOp<*> && e.columnType is JsonColumnMarker -> when { + currentDialect is MariaDBDialect -> super.processForDefaultValue(e) + (currentDialect as? MysqlDialect)?.isMysql8 == true -> "(${super.processForDefaultValue(e)})" + else -> throw UnsupportedByDialectException( + "MySQL versions prior to 8.0.13 do not accept default values on JSON columns", + currentDialect + ) + } + else -> super.processForDefaultValue(e) + } + + override fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) { when (sortOrder) { SortOrder.ASC, SortOrder.DESC -> super.precessOrderByClause(queryBuilder, expression, sortOrder) SortOrder.ASC_NULLS_FIRST -> super.precessOrderByClause(queryBuilder, expression, SortOrder.ASC) @@ -96,11 +121,58 @@ internal open class MysqlFunctionProvider : FunctionProvider() { } } + override fun jsonExtract( + expression: Expression, + vararg path: String, + toScalar: Boolean, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) = queryBuilder { + if (toScalar) append("JSON_UNQUOTE(") + append("JSON_EXTRACT(", expression, ", ") + path.ifEmpty { arrayOf("") }.appendTo { +"\"$$it\"" } + append(")${if (toScalar) ")" else ""}") + } + + override fun jsonContains( + target: Expression<*>, + candidate: Expression<*>, + path: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) = queryBuilder { + append("JSON_CONTAINS(", target, ", ", candidate) + path?.let { + append(", '$$it'") + } + append(")") + } + + override fun jsonExists( + expression: Expression<*>, + vararg path: String, + optional: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + val oneOrAll = optional?.lowercase() + if (oneOrAll != "one" && oneOrAll != "all") { + TransactionManager.current().throwUnsupportedException("MySQL requires a single optional argument: 'one' or 'all'") + } + queryBuilder { + append("JSON_CONTAINS_PATH(", expression, ", ") + append("'$oneOrAll', ") + path.ifEmpty { arrayOf("") }.appendTo { +"'$$it'" } + append(")") + } + } + override fun replace( table: Table, columns: List>, expression: String, - transaction: Transaction + transaction: Transaction, + prepared: Boolean ): String { val insertStatement = super.insert(false, table, columns, expression, transaction) return insertStatement.replace("INSERT", "REPLACE") @@ -167,7 +239,7 @@ internal open class MysqlFunctionProvider : FunctionProvider() { val isAliasSupported = when (val dialect = transaction.db.dialect) { is MysqlDialect -> dialect !is MariaDBDialect && dialect.isMysql8 - else -> false // H2_MySQL mode also uses this function provider & requires older version + else -> false // H2_MySQL mode also uses this function provider & requires older version } return with(QueryBuilder(true)) { @@ -178,7 +250,7 @@ internal open class MysqlFunctionProvider : FunctionProvider() { +" ON DUPLICATE KEY UPDATE " onUpdate?.appendTo { (columnToUpdate, updateExpression) -> - append("${transaction.identity(columnToUpdate)}=${updateExpression}") + append("${transaction.identity(columnToUpdate)}=$updateExpression") } ?: run { data.unzip().first.appendTo { column -> val columnName = transaction.identity(column) @@ -205,12 +277,19 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq override val supportsCreateSequence: Boolean = false + override val supportsTernaryAffectedRowValues: Boolean = true + override val supportsSubqueryUnions: Boolean = true override val supportsOrderByNullsFirstLast: Boolean = false + override val supportsSetDefaultReferenceOption: Boolean = false + fun isFractionDateTimeSupported(): Boolean = TransactionManager.current().db.isVersionCovers(BigDecimal("5.6")) + // Available from MySQL 8.0.19 + fun isTimeZoneOffsetSupported(): Boolean = (currentDialect !is MariaDBDialect) && isMysql8 + override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean { if (super.isAllowedAsColumnDefault(e)) return true val acceptableDefaults = arrayOf("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "NOW()", "CURRENT_TIMESTAMP(6)", "NOW(6)") @@ -218,11 +297,11 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq } override fun fillConstraintCacheForTables(tables: List
) { - val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCase() } + val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCaseUnquoted() } val allTableNames = allTables.keys val inTableList = allTableNames.joinToString("','", prefix = " ku.TABLE_NAME IN ('", postfix = "')") val tr = TransactionManager.current() - val schemaName = "'${getDatabase()}'" + val tableSchema = "'${tables.mapNotNull { it.schemaName }.toSet().singleOrNull() ?: getDatabase()}'" val constraintsToLoad = HashMap>() tr.exec( """SELECT @@ -236,9 +315,9 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku ON ku.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA AND rc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME - WHERE ku.TABLE_SCHEMA = $schemaName - AND ku.CONSTRAINT_SCHEMA = $schemaName - AND rc.CONSTRAINT_SCHEMA = $schemaName + WHERE ku.TABLE_SCHEMA = $tableSchema + AND ku.CONSTRAINT_SCHEMA = $tableSchema + AND rc.CONSTRAINT_SCHEMA = $tableSchema AND $inTableList ORDER BY ku.ORDINAL_POSITION """.trimIndent() @@ -276,7 +355,18 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq } } - override fun dropIndex(tableName: String, indexName: String): String = "ALTER TABLE $tableName DROP INDEX $indexName" + override fun createIndex(index: Index): String { + if (index.functions != null && !isMysql8) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in MySQL prior to 8.0" + ) + return "" + } + return super.createIndex(index) + } + + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String = + "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP INDEX ${identifierManager.quoteIfNecessary(indexName)}" override fun setSchema(schema: Schema): String = "USE ${schema.identifier}" @@ -294,5 +384,23 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq override fun dropSchema(schema: Schema, cascade: Boolean): String = "DROP SCHEMA IF EXISTS ${schema.identifier}" - companion object : DialectNameProvider("mysql") + 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 3694f5302c..fa41799995 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 @@ -3,10 +3,13 @@ package org.jetbrains.exposed.sql.vendors import org.jetbrains.exposed.exceptions.throwUnsupportedException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.sql.DatabaseMetaData +import java.util.* internal object OracleDataTypeProvider : DataTypeProvider() { override fun byteType(): String = "SMALLINT" - override fun ubyteType(): String = "SMALLINT" + override fun ubyteType(): String = "NUMBER(4)" + override fun ushortType(): String = "NUMBER(6)" override fun integerType(): String = "NUMBER(12)" override fun integerAutoincType(): String = "NUMBER(12)" override fun uintegerType(): String = "NUMBER(13)" @@ -25,20 +28,41 @@ internal object OracleDataTypeProvider : DataTypeProvider() { override fun binaryType(length: Int): String { @Suppress("MagicNumber") - return if (length < 2000) "RAW ($length)" - else binaryType() + return if (length < 2000) "RAW ($length)" else binaryType() + } + + override fun uuidType(): String { + return if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { + "UUID" + } else { + return "RAW(16)" + } + } + + override fun uuidToDB(value: UUID): Any { + return if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { + H2DataTypeProvider.uuidToDB(value) + } else { + super.uuidToDB(value) + } } - override fun uuidType(): String = "RAW(16)" override fun dateTimeType(): String = "TIMESTAMP" override fun booleanType(): String = "CHAR(1)" override fun booleanToStatementString(bool: Boolean) = if (bool) "1" else "0" override fun booleanFromStringToBoolean(value: String): Boolean = try { value.toLong() != 0L } catch (ex: NumberFormatException) { - error("Unexpected value of type Boolean: $value") + @Suppress("SwallowedException") + try { + value.lowercase().toBooleanStrict() + } catch (ex: IllegalArgumentException) { + error("Unexpected value of type Boolean: $value") + } } + override fun jsonType(): String = "VARCHAR2(4000)" + override fun processForDefaultValue(e: Expression<*>): String = when { e is LiteralOp<*> && (e.columnType as? IDateColumnType)?.hasTimePart == false -> "DATE ${super.processForDefaultValue(e)}" e is LiteralOp<*> && e.columnType is IDateColumnType -> "TIMESTAMP ${super.processForDefaultValue(e)}" @@ -142,6 +166,44 @@ internal object OracleFunctionProvider : FunctionProvider() { append(")") } + override fun jsonExtract( + expression: Expression, + vararg path: String, + toScalar: Boolean, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + if (path.size > 1) { + TransactionManager.current().throwUnsupportedException("Oracle does not support multiple JSON path arguments") + } + queryBuilder { + append(if (toScalar) "JSON_VALUE" else "JSON_QUERY") + append("(", expression, ", ") + append("'$", path.firstOrNull() ?: "", "'") + append(")") + } + } + + override fun jsonExists( + expression: Expression<*>, + vararg path: String, + optional: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + if (path.size > 1) { + TransactionManager.current().throwUnsupportedException("Oracle does not support multiple JSON path arguments") + } + queryBuilder { + append("JSON_EXISTS(", expression, ", ") + append("'$", path.firstOrNull() ?: "", "'") + optional?.let { + append(" $it") + } + append(")") + } + } + override fun update( target: Table, columnsAndValues: List, Any?>>, @@ -191,7 +253,7 @@ internal object OracleFunctionProvider : FunctionProvider() { } limit?.let { - "WHERE ROWNUM <= $it" + +" WHERE ROWNUM <= $it" } toString() @@ -249,9 +311,18 @@ open class OracleDialect : VendorDialect(dialectName, OracleDataTypeProvider, Or override val supportsOnlyIdentifiersInGeneratedKeys: Boolean = true override val supportsDualTableConcept: Boolean = true override val supportsOrderByNullsFirstLast: Boolean = true + override val supportsOnUpdate: Boolean = false + override val supportsSetDefaultReferenceOption: Boolean = false + + // Preventing the deletion of a parent row if a child row references it is the default behaviour in Oracle. + override val supportsRestrictReferenceOption: Boolean = false override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String { + return "DROP INDEX ${identifierManager.quoteIfNecessary(indexName)}" + } + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List { val result = super.modifyColumn(column, columnDiff).map { it.replace("MODIFY COLUMN", "MODIFY") @@ -268,12 +339,15 @@ open class OracleDialect : VendorDialect(dialectName, OracleDataTypeProvider, Or override fun createDatabase(name: String): String = "CREATE DATABASE ${name.inProperCase()}" - override fun dropDatabase(name: String): String = "DROP 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}" override fun createSchema(schema: Schema): String = buildString { if ((schema.quota == null) xor (schema.on == null)) { + @Suppress("UseRequire") throw IllegalArgumentException("You must either provide both and options or non of them") } @@ -293,5 +367,16 @@ open class OracleDialect : VendorDialect(dialectName, OracleDataTypeProvider, Or } } - companion object : DialectNameProvider("oracle") + /** + * The SQL that gets the constraint information for Oracle returns a 1 for NO ACTION and does not support RESTRICT. + * `decode (f.delete_rule, 'CASCADE', 0, 'SET NULL', 2, 1) as delete_rule` + */ + override fun resolveRefOptionFromJdbc(refOption: Int): ReferenceOption = when (refOption) { + DatabaseMetaData.importedKeyCascade -> ReferenceOption.CASCADE + DatabaseMetaData.importedKeySetNull -> ReferenceOption.SET_NULL + DatabaseMetaData.importedKeyRestrict -> ReferenceOption.NO_ACTION + else -> currentDialect.defaultReferenceOption + } + + companion object : DialectNameProvider("Oracle") } 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 8685b1b801..8946649cb6 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 @@ -19,7 +19,16 @@ internal object PostgreSQLDataTypeProvider : DataTypeProvider() { override fun blobType(): String = "bytea" override fun uuidToDB(value: UUID): Any = value override fun dateTimeType(): String = "TIMESTAMP" - override fun ubyteType(): String = "SMALLINT" + override fun jsonBType(): String = "JSONB" + + override fun processForDefaultValue(e: Expression<*>): String = when { + e is LiteralOp<*> && e.columnType is JsonColumnMarker && (currentDialect as? H2Dialect) == null -> { + val cast = if (e.columnType.usesBinaryFormat) "::jsonb" else "::json" + "${super.processForDefaultValue(e)}$cast" + } + else -> super.processForDefaultValue(e) + } + override fun hexToDb(hexString: String): String = """E'\\x$hexString'""" } @@ -110,6 +119,65 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { append(")") } + override fun jsonExtract( + expression: Expression, + vararg path: String, + toScalar: Boolean, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) = queryBuilder { + append("${jsonType.sqlType()}_EXTRACT_PATH") + if (toScalar) append("_TEXT") + append("(", expression, ", ") + path.ifEmpty { arrayOf("$") }.appendTo { +"'$it'" } + append(")") + } + + override fun jsonContains( + target: Expression<*>, + candidate: Expression<*>, + path: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + path?.let { + TransactionManager.current().throwUnsupportedException("PostgreSQL does not support a JSON path argument") + } + val isNotJsonB = !(jsonType as JsonColumnMarker).usesBinaryFormat + queryBuilder { + append(target) + if (isNotJsonB) append("::jsonb") + append(" @> ", candidate) + if (isNotJsonB) append("::jsonb") + } + } + + override fun jsonExists( + expression: Expression<*>, + vararg path: String, + optional: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + if (path.size > 1) { + TransactionManager.current().throwUnsupportedException("PostgreSQL does not support multiple JSON path arguments") + } + val isNotJsonB = !(jsonType as JsonColumnMarker).usesBinaryFormat + queryBuilder { + append("JSONB_PATH_EXISTS(") + if (isNotJsonB) { + append("CAST(", expression, " as jsonb), ") + } else { + append(expression, ", ") + } + append("'$", path.firstOrNull() ?: "", "'") + optional?.let { + append(", '$it'") + } + append(")") + } + } + private const val onConflictIgnore = "ON CONFLICT DO NOTHING" override fun insert( @@ -167,7 +235,7 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { } toString() } - + override fun upsert( table: Table, data: List, Any?>>, @@ -219,48 +287,62 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { /** * PostgreSQL dialect implementation. */ -open class PostgreSQLDialect : VendorDialect(dialectName, PostgreSQLDataTypeProvider, PostgreSQLFunctionProvider) { +open class PostgreSQLDialect(override val name: String = dialectName) : VendorDialect(dialectName, PostgreSQLDataTypeProvider, PostgreSQLFunctionProvider) { override val supportsOrderByNullsFirstLast: Boolean = true override val requiresAutoCommitOnCreateDrop: Boolean = true + override val supportsWindowFrameGroupsMode: Boolean = true + override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true - override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = listOf(buildString { - val tr = TransactionManager.current() - append("ALTER TABLE ${tr.identity(column.table)} ") - val colName = tr.identity(column) - append("ALTER COLUMN $colName TYPE ${column.columnType.sqlType()}") - - if (columnDiff.nullability) { - append(", ALTER COLUMN $colName ") - if (column.columnType.nullable) { - append("DROP ") - } else { - append("SET ") + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = listOf( + buildString { + val tr = TransactionManager.current() + append("ALTER TABLE ${tr.identity(column.table)} ") + val colName = tr.identity(column) + append("ALTER COLUMN $colName TYPE ${column.columnType.sqlType()}") + + if (columnDiff.nullability) { + append(", ALTER COLUMN $colName ") + if (column.columnType.nullable) { + append("DROP ") + } else { + append("SET ") + } + append("NOT NULL") } - append("NOT NULL") - } - if (columnDiff.defaults) { - column.dbDefaultValue?.let { - append(", ALTER COLUMN $colName SET DEFAULT ${PostgreSQLDataTypeProvider.processForDefaultValue(it)}") - } ?: run { - append(", ALTER COLUMN $colName DROP DEFAULT") + if (columnDiff.defaults) { + column.dbDefaultValue?.let { + append(", ALTER COLUMN $colName SET DEFAULT ${PostgreSQLDataTypeProvider.processForDefaultValue(it)}") + } ?: run { + append(", ALTER COLUMN $colName DROP DEFAULT") + } } } - }) + ) 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}" - override fun createIndexWithType(name: String, table: String, columns: String, type: String): String { - return "CREATE INDEX $name ON $table USING $type $columns" + override fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { + return "CREATE INDEX $name ON $table USING $type $columns$filterCondition" + } + + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String { + return if (isUnique && !isPartialOrFunctional) { + "ALTER TABLE IF EXISTS ${identifierManager.quoteIfNecessary(tableName)} DROP CONSTRAINT IF EXISTS ${identifierManager.quoteIfNecessary(indexName)}" + } else { + "DROP INDEX IF EXISTS ${identifierManager.quoteIfNecessary(indexName)}" + } } - companion object : DialectNameProvider("postgresql") + companion object : DialectNameProvider("PostgreSQL") } /** @@ -268,8 +350,8 @@ open class PostgreSQLDialect : VendorDialect(dialectName, PostgreSQLDataTypeProv * * The driver accepts basic URLs in the following format : jdbc:pgsql://localhost:5432/db */ -open class PostgreSQLNGDialect : PostgreSQLDialect() { +open class PostgreSQLNGDialect : PostgreSQLDialect(dialectName) { override val requiresAutoCommitOnCreateDrop: Boolean = true - companion object : DialectNameProvider("pgsql") + companion object : DialectNameProvider("PostgreSQLNG") } 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 a0b5101917..fb6e4b5d37 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 @@ -6,6 +6,13 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import java.util.* internal object SQLServerDataTypeProvider : DataTypeProvider() { + override fun ubyteType(): String { + return if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { + "SMALLINT" + } else { + "TINYINT" + } + } override fun integerAutoincType(): String = "INT IDENTITY(1,1)" override fun longAutoincType(): String = "BIGINT IDENTITY(1,1)" override fun binaryType(): String { @@ -17,6 +24,12 @@ internal object SQLServerDataTypeProvider : DataTypeProvider() { override fun uuidType(): String = "uniqueidentifier" override fun uuidToDB(value: UUID): Any = value.toString() override fun dateTimeType(): String = "DATETIME2" + override fun timestampWithTimeZoneType(): String = + if ((currentDialect as? H2Dialect)?.h2Mode == H2Dialect.H2CompatibilityMode.SQLServer) { + "TIMESTAMP(9) WITH TIME ZONE" + } else { + "DATETIMEOFFSET" + } override fun booleanType(): String = "BIT" override fun booleanToStatementString(bool: Boolean): String = if (bool) "1" else "0" @@ -27,6 +40,7 @@ internal object SQLServerDataTypeProvider : DataTypeProvider() { override fun textType(): String = "VARCHAR(MAX)" override fun mediumTextType(): String = textType() override fun largeTextType(): String = textType() + override fun jsonType(): String = "NVARCHAR(MAX)" override fun precessOrderByClause(queryBuilder: QueryBuilder, expression: Expression<*>, sortOrder: SortOrder) { when (sortOrder) { @@ -134,6 +148,24 @@ internal object SQLServerFunctionProvider : FunctionProvider() { append("VAR(", expression, ")") } + override fun jsonExtract( + expression: Expression, + vararg path: String, + toScalar: Boolean, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + if (path.size > 1) { + TransactionManager.current().throwUnsupportedException("SQLServer does not support multiple JSON path arguments") + } + queryBuilder { + append(if (toScalar) "JSON_VALUE" else "JSON_QUERY") + append("(", expression, ", ") + path.ifEmpty { arrayOf("") }.appendTo { +"'$$it'" } + append(")") + } + } + override fun update( target: Table, columnsAndValues: List, Any?>>, @@ -211,6 +243,7 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid override val needsQuotesWhenSymbolsInNames: Boolean = false override val supportsSequenceAsGeneratedKeys: Boolean = false override val supportsOnlyIdentifiersInGeneratedKeys: Boolean = true + override val supportsRestrictReferenceOption: Boolean = false private val nonAcceptableDefaults = arrayOf("DEFAULT") @@ -219,13 +252,60 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid return columnDefault !in nonAcceptableDefaults } - // TODO: Fix changing default value on column as it requires to drop/create constraint - // https://stackoverflow.com/questions/15547210/modify-default-value-in-sql-server - override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = - super.modifyColumn(column, columnDiff).map { it.replace("MODIFY COLUMN", "ALTER COLUMN") } + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List { + val transaction = TransactionManager.current() + + val alterTablePart = "ALTER TABLE ${transaction.identity(column.table)} " + + val statements = mutableListOf() + + statements.add( + buildString { + append(alterTablePart + "ALTER COLUMN ${transaction.identity(column)} ${column.columnType.sqlType()}") + + if (columnDiff.nullability) { + val defaultValue = column.dbDefaultValue + val isPKColumn = column.table.primaryKey?.columns?.contains(column) == true + + if (column.columnType.nullable || + (defaultValue != null && column.defaultValueFun == null && !currentDialect.isAllowedAsColumnDefault(defaultValue)) + ) { + append(" NULL") + } else if (!isPKColumn) { + append(" NOT NULL") + } + } + } + ) + + if (columnDiff.defaults) { + val tableName = column.table.tableName + val columnName = column.name + val constraintName = "DF_${tableName}_$columnName" + + val dropConstraint = "DROP CONSTRAINT IF EXISTS $constraintName" + + statements.add( + buildString { + column.dbDefaultValue?.let { + append(alterTablePart + dropConstraint) + append("; ") + append( + alterTablePart + + "ADD CONSTRAINT $constraintName DEFAULT ${SQLServerDataTypeProvider.processForDefaultValue(it)} for ${transaction.identity(column)}" + ) + } ?: append(alterTablePart + dropConstraint) + } + ) + } + + return statements + } 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}" @@ -235,22 +315,40 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid appendIfNotNull(" AUTHORIZATION ", schema.authorization) } - override fun dropSchema(schema: Schema, cascade: Boolean): String = buildString { - append("DROP SCHEMA ", schema.identifier) + override fun dropSchema(schema: Schema, cascade: Boolean): String = "DROP SCHEMA ${schema.identifier}" - if (cascade) { - append(" CASCADE") + override fun createIndex(index: Index): String { + if (index.functions != null) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in SQLServer" + ) + return "" } + return super.createIndex(index) + } + + override fun createIndexWithType( + name: String, + table: String, + columns: String, + type: String, + filterCondition: String + ): String { + return "CREATE $type INDEX $name ON $table $columns$filterCondition" } - override fun createIndexWithType(name: String, table: String, columns: String, type: String): String { - return "CREATE $type INDEX $name ON $table $columns" + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String { + return if (isUnique && !isPartialOrFunctional) { + "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP CONSTRAINT IF EXISTS ${identifierManager.quoteIfNecessary(indexName)}" + } else { + "DROP INDEX IF EXISTS ${identifierManager.quoteIfNecessary(indexName)} ON ${identifierManager.quoteIfNecessary(tableName)}" + } } // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/like-transact-sql?redirectedfrom=MSDN&view=sql-server-ver15#arguments override val likePatternSpecialChars = sqlServerLikePatternSpecialChars - companion object : DialectNameProvider("sqlserver") { + companion object : DialectNameProvider("SQLServer") { private val sqlServerLikePatternSpecialChars = mapOf('%' to null, '_' to null, '[' to ']') } } 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 843d6ac6a0..31410675b2 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 @@ -15,8 +15,10 @@ internal object SQLiteDataTypeProvider : DataTypeProvider() { override fun floatType(): String = "SINGLE" override fun binaryType(): String = "BLOB" override fun dateTimeType(): String = "TEXT" + override fun timestampWithTimeZoneType(): String = "TEXT" override fun dateType(): String = "TEXT" override fun booleanToStatementString(bool: Boolean) = if (bool) "1" else "0" + override fun jsonType(): String = "TEXT" override fun hexToDb(hexString: String): String = "X'$hexString'" } @@ -127,6 +129,39 @@ internal object SQLiteFunctionProvider : FunctionProvider() { queryBuilder: QueryBuilder ): Unit = TransactionManager.current().throwUnsupportedException("$UNSUPPORTED_AGGREGATE VAR_SAMP") + override fun jsonExtract( + expression: Expression, + vararg path: String, + toScalar: Boolean, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) = queryBuilder { + append("JSON_EXTRACT(", expression, ", ") + path.ifEmpty { arrayOf("") }.appendTo { +"'$$it'" } + append(")") + } + + override fun jsonExists( + expression: Expression<*>, + vararg path: String, + optional: String?, + jsonType: IColumnType, + queryBuilder: QueryBuilder + ) { + val transaction = TransactionManager.current() + if (path.size > 1) { + transaction.throwUnsupportedException("SQLite does not support multiple JSON path arguments") + } + optional?.let { + transaction.throwUnsupportedException("SQLite does not support optional arguments other than a path argument") + } + queryBuilder { + append("JSON_TYPE(", expression, ", ") + append("'$", path.firstOrNull() ?: "", "'") + append(") IS NOT NULL") + } + } + override fun insert( ignore: Boolean, table: Table, @@ -155,7 +190,8 @@ internal object SQLiteFunctionProvider : FunctionProvider() { table: Table, columns: List>, expression: String, - transaction: Transaction + transaction: Transaction, + prepared: Boolean ): String { val insertStatement = super.insert(false, table, columns, expression, transaction) return insertStatement.replace("INSERT", "INSERT OR REPLACE") @@ -212,6 +248,7 @@ open class SQLiteDialect : VendorDialect(dialectName, SQLiteDataTypeProvider, SQ override val supportsCreateSequence: Boolean = false override val supportsMultipleGeneratedKeys: Boolean = false override val supportsCreateSchema: Boolean = false + override val supportsWindowFrameGroupsMode: Boolean = true override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true @@ -230,11 +267,17 @@ open class SQLiteDialect : VendorDialect(dialectName, SQLiteDataTypeProvider, SQ } } + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartialOrFunctional: Boolean): String { + return "DROP INDEX IF EXISTS ${identifierManager.quoteIfNecessary(indexName)}" + } + 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") { + companion object : DialectNameProvider("SQLite") { val ENABLE_UPDATE_DELETE_LIMIT by lazy { var conn: Connection? = null var stmt: Statement? = null 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-crypt/api/exposed-crypt.api b/exposed-crypt/api/exposed-crypt.api new file mode 100644 index 0000000000..227face956 --- /dev/null +++ b/exposed-crypt/api/exposed-crypt.api @@ -0,0 +1,41 @@ +public final class org/jetbrains/exposed/crypt/Algorithms { + public static final field INSTANCE Lorg/jetbrains/exposed/crypt/Algorithms; + public final fun AES_256_PBE_CBC (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Lorg/jetbrains/exposed/crypt/Encryptor; + public final fun AES_256_PBE_GCM (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Lorg/jetbrains/exposed/crypt/Encryptor; + public final fun BLOW_FISH (Ljava/lang/CharSequence;)Lorg/jetbrains/exposed/crypt/Encryptor; + public final fun TRIPLE_DES (Ljava/lang/CharSequence;)Lorg/jetbrains/exposed/crypt/Encryptor; +} + +public final class org/jetbrains/exposed/crypt/EncryptedBinaryColumnType : org/jetbrains/exposed/sql/BinaryColumnType { + public fun (Lorg/jetbrains/exposed/crypt/Encryptor;I)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/crypt/EncryptedVarCharColumnType : org/jetbrains/exposed/sql/VarCharColumnType { + public fun (Lorg/jetbrains/exposed/crypt/Encryptor;I)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun validateValueBeforeUpdate (Ljava/lang/Object;)V + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/crypt/Encryptor { + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public final fun decrypt (Ljava/lang/String;)Ljava/lang/String; + public final fun encrypt (Ljava/lang/String;)Ljava/lang/String; + public final fun getDecryptFn ()Lkotlin/jvm/functions/Function1; + public final fun getEncryptFn ()Lkotlin/jvm/functions/Function1; + public final fun getMaxColLengthFn ()Lkotlin/jvm/functions/Function1; + public final fun maxColLength (I)I +} + +public final class org/jetbrains/exposed/crypt/TablesKt { + public static final fun encryptedBinary (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;ILorg/jetbrains/exposed/crypt/Encryptor;)Lorg/jetbrains/exposed/sql/Column; + public static final fun encryptedVarchar (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;ILorg/jetbrains/exposed/crypt/Encryptor;)Lorg/jetbrains/exposed/sql/Column; +} + diff --git a/exposed-crypt/build.gradle.kts b/exposed-crypt/build.gradle.kts index 5cd32f9189..a895cddce2 100644 --- a/exposed-crypt/build.gradle.kts +++ b/exposed-crypt/build.gradle.kts @@ -6,6 +6,10 @@ repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) api("org.springframework.security", "spring-security-crypto", "5.7.3") diff --git a/exposed-crypt/src/main/kotlin/org/jetbrains/exposed/crypt/Algorithms.kt b/exposed-crypt/src/main/kotlin/org/jetbrains/exposed/crypt/Algorithms.kt index a62f259d51..d86dbe881b 100644 --- a/exposed-crypt/src/main/kotlin/org/jetbrains/exposed/crypt/Algorithms.kt +++ b/exposed-crypt/src/main/kotlin/org/jetbrains/exposed/crypt/Algorithms.kt @@ -10,6 +10,7 @@ import javax.crypto.spec.SecretKeySpec import kotlin.math.ceil object Algorithms { + @Suppress("MagicNumber") private fun base64EncodedLength(byteSize: Int): Int = ceil(byteSize.toDouble() / 3).toInt() * 4 private fun paddingLen(len: Int, blockSize: Int): Int = if (len % blockSize == 0) 0 else blockSize - len % blockSize private val base64Decoder = Base64.getDecoder() @@ -17,38 +18,47 @@ object Algorithms { private const val AES_256_GCM_BLOCK_LENGTH = 16 private const val AES_256_GCM_TAG_LENGTH = 16 + @Suppress("FunctionNaming") fun AES_256_PBE_GCM(password: CharSequence, salt: CharSequence): Encryptor { - - return AesBytesEncryptor(password.toString(), - salt, - KeyGenerators.secureRandom(AES_256_GCM_BLOCK_LENGTH), - AesBytesEncryptor.CipherAlgorithm.GCM) - .run { - Encryptor({ base64Encoder.encodeToString(encrypt(it.toByteArray())) }, - { String(decrypt(base64Decoder.decode(it))) }, - { inputLen -> - base64EncodedLength(AES_256_GCM_BLOCK_LENGTH + inputLen + AES_256_GCM_TAG_LENGTH) }) - } + return AesBytesEncryptor( + password.toString(), + salt, + KeyGenerators.secureRandom(AES_256_GCM_BLOCK_LENGTH), + AesBytesEncryptor.CipherAlgorithm.GCM + ).run { + Encryptor( + { base64Encoder.encodeToString(encrypt(it.toByteArray())) }, + { String(decrypt(base64Decoder.decode(it))) }, + { inputLen -> + base64EncodedLength(AES_256_GCM_BLOCK_LENGTH + inputLen + AES_256_GCM_TAG_LENGTH) + } + ) + } } private const val AES_256_CBC_BLOCK_LENGTH = 16 + @Suppress("FunctionNaming") fun AES_256_PBE_CBC(password: CharSequence, salt: CharSequence): Encryptor { - - return AesBytesEncryptor(password.toString(), - salt, - KeyGenerators.secureRandom(AES_256_CBC_BLOCK_LENGTH)) - .run { - Encryptor({ base64Encoder.encodeToString(encrypt(it.toByteArray())) }, - { String(decrypt(base64Decoder.decode(it))) }, - { inputLen -> - val paddingSize = (AES_256_CBC_BLOCK_LENGTH - inputLen % AES_256_CBC_BLOCK_LENGTH) - base64EncodedLength(AES_256_CBC_BLOCK_LENGTH + inputLen + paddingSize) }) - } + return AesBytesEncryptor( + password.toString(), + salt, + KeyGenerators.secureRandom(AES_256_CBC_BLOCK_LENGTH) + ).run { + Encryptor( + { base64Encoder.encodeToString(encrypt(it.toByteArray())) }, + { String(decrypt(base64Decoder.decode(it))) }, + { inputLen -> + val paddingSize = (AES_256_CBC_BLOCK_LENGTH - inputLen % AES_256_CBC_BLOCK_LENGTH) + base64EncodedLength(AES_256_CBC_BLOCK_LENGTH + inputLen + paddingSize) + } + ) + } } private const val BLOW_FISH_BLOCK_LENGTH = 8 + @Suppress("FunctionNaming") fun BLOW_FISH(key: CharSequence): Encryptor { val ks = SecretKeySpec(key.toString().toByteArray(), "Blowfish") @@ -68,12 +78,14 @@ object Algorithms { val decryptedBytes = cipher.doFinal(base64Decoder.decode(encryptedText)) String(decryptedBytes) }, - { base64EncodedLength(it + paddingLen(it, BLOW_FISH_BLOCK_LENGTH)) }) + { base64EncodedLength(it + paddingLen(it, BLOW_FISH_BLOCK_LENGTH)) } + ) } private const val TRIPLE_DES_KEY_LENGTH = 24 private const val TRIPLE_DES_BLOCK_LENGTH = 8 - @Suppress("FunctionNaming") + + @Suppress("FunctionNaming", "UseRequire") fun TRIPLE_DES(secretKey: CharSequence): Encryptor { if (secretKey.toString().toByteArray().size != TRIPLE_DES_KEY_LENGTH) { throw IllegalArgumentException("secretKey must have 24 bytes") @@ -101,6 +113,7 @@ object Algorithms { val decryptedBytes = cipher.doFinal(EncodingUtils.subArray(decodedBytes, TRIPLE_DES_BLOCK_LENGTH, decodedBytes.size)) String(decryptedBytes) }, - { base64EncodedLength(TRIPLE_DES_BLOCK_LENGTH + it + paddingLen(it, TRIPLE_DES_BLOCK_LENGTH)) }) + { base64EncodedLength(TRIPLE_DES_BLOCK_LENGTH + it + paddingLen(it, TRIPLE_DES_BLOCK_LENGTH)) } + ) } } diff --git a/exposed-dao/api/exposed-dao.api b/exposed-dao/api/exposed-dao.api new file mode 100644 index 0000000000..ca8e0333ff --- /dev/null +++ b/exposed-dao/api/exposed-dao.api @@ -0,0 +1,314 @@ +public class org/jetbrains/exposed/dao/ColumnWithTransform { + public fun (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Z)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected final fun getCacheResult ()Z + public final fun getColumn ()Lorg/jetbrains/exposed/sql/Column; + public final fun getToColumn ()Lkotlin/jvm/functions/Function1; + public final fun getToReal ()Lkotlin/jvm/functions/Function1; +} + +public final class org/jetbrains/exposed/dao/DaoEntityID : org/jetbrains/exposed/dao/id/EntityID { + public fun (Ljava/lang/Comparable;Lorg/jetbrains/exposed/dao/id/IdTable;)V +} + +public final class org/jetbrains/exposed/dao/DaoEntityIDFactory : org/jetbrains/exposed/dao/id/EntityIDFactory { + public fun ()V + public fun createEntityID (Ljava/lang/Comparable;Lorg/jetbrains/exposed/dao/id/IdTable;)Lorg/jetbrains/exposed/dao/id/EntityID; +} + +public class org/jetbrains/exposed/dao/Entity { + public fun (Lorg/jetbrains/exposed/dao/id/EntityID;)V + public fun delete ()V + public fun flush (Lorg/jetbrains/exposed/dao/EntityBatchUpdate;)Z + public static synthetic fun flush$default (Lorg/jetbrains/exposed/dao/Entity;Lorg/jetbrains/exposed/dao/EntityBatchUpdate;ILjava/lang/Object;)Z + public final fun getDb ()Lorg/jetbrains/exposed/sql/Database; + public final fun getId ()Lorg/jetbrains/exposed/dao/id/EntityID; + public final fun getKlass ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getReadValues ()Lorg/jetbrains/exposed/sql/ResultRow; + public final fun getValue (Lorg/jetbrains/exposed/dao/ColumnWithTransform;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public final fun getValue (Lorg/jetbrains/exposed/dao/OptionalReference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/dao/Entity; + public final fun getValue (Lorg/jetbrains/exposed/dao/Reference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/dao/Entity; + public final fun getValue (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public final fun getValue (Lorg/jetbrains/exposed/sql/CompositeColumn;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public final fun getWriteValues ()Ljava/util/LinkedHashMap; + public final fun get_readValues ()Lorg/jetbrains/exposed/sql/ResultRow; + public final fun lookup (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Object; + public final fun lookupInReadValues (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public fun refresh (Z)V + public static synthetic fun refresh$default (Lorg/jetbrains/exposed/dao/Entity;ZILjava/lang/Object;)V + public final fun setValue (Lorg/jetbrains/exposed/dao/ColumnWithTransform;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V + public final fun setValue (Lorg/jetbrains/exposed/dao/OptionalReference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Lorg/jetbrains/exposed/dao/Entity;)V + public final fun setValue (Lorg/jetbrains/exposed/dao/Reference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Lorg/jetbrains/exposed/dao/Entity;)V + public final fun setValue (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V + public final fun setValue (Lorg/jetbrains/exposed/sql/CompositeColumn;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V + public final fun set_readValues (Lorg/jetbrains/exposed/sql/ResultRow;)V + public final fun storeWrittenValues ()V + public final fun via (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/InnerTableLink; + public final fun via (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Table;)Lorg/jetbrains/exposed/dao/InnerTableLink; +} + +public final class org/jetbrains/exposed/dao/EntityBatchUpdate { + public fun (Lorg/jetbrains/exposed/dao/EntityClass;)V + public final fun addBatch (Lorg/jetbrains/exposed/dao/Entity;)V + public final fun execute (Lorg/jetbrains/exposed/sql/Transaction;)I + public final fun set (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)V +} + +public final class org/jetbrains/exposed/dao/EntityCache { + public static final field Companion Lorg/jetbrains/exposed/dao/EntityCache$Companion; + public fun (Lorg/jetbrains/exposed/sql/Transaction;)V + public final fun clear (Z)V + public static synthetic fun clear$default (Lorg/jetbrains/exposed/dao/EntityCache;ZILjava/lang/Object;)V + public final fun clearReferrersCache ()V + public final fun find (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/EntityID;)Lorg/jetbrains/exposed/dao/Entity; + public final fun findAll (Lorg/jetbrains/exposed/dao/EntityClass;)Ljava/util/Collection; + public final fun flush ()V + public final fun flush (Ljava/lang/Iterable;)V + public final fun getData ()Ljava/util/LinkedHashMap; + public final fun getMaxEntitiesToStore ()I + public final fun getOrPutReferrers (Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun getReferrers (Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun remove (Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/dao/Entity;)V + public final fun scheduleInsert (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/Entity;)V + public final fun scheduleUpdate (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/Entity;)V + public final fun setMaxEntitiesToStore (I)V + public final fun store (Lorg/jetbrains/exposed/dao/Entity;)V + public final fun store (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/Entity;)V +} + +public final class org/jetbrains/exposed/dao/EntityCache$Companion { + public final fun invalidateGlobalCaches (Ljava/util/List;)V +} + +public final class org/jetbrains/exposed/dao/EntityCacheKt { + public static final fun flushCache (Lorg/jetbrains/exposed/sql/Transaction;)Ljava/util/List; + public static final fun getEntityCache (Lorg/jetbrains/exposed/sql/Transaction;)Lorg/jetbrains/exposed/dao/EntityCache; +} + +public final class org/jetbrains/exposed/dao/EntityChange { + public fun (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/dao/EntityChangeType;Ljava/lang/String;)V + public final fun component1 ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun component2 ()Lorg/jetbrains/exposed/dao/id/EntityID; + public final fun component3 ()Lorg/jetbrains/exposed/dao/EntityChangeType; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/dao/EntityChangeType;Ljava/lang/String;)Lorg/jetbrains/exposed/dao/EntityChange; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/dao/EntityChange;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/dao/EntityChangeType;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/dao/EntityChange; + public fun equals (Ljava/lang/Object;)Z + public final fun getChangeType ()Lorg/jetbrains/exposed/dao/EntityChangeType; + public final fun getEntityClass ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getEntityId ()Lorg/jetbrains/exposed/dao/id/EntityID; + public final fun getTransactionId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/dao/EntityChangeType : java/lang/Enum { + public static final field Created Lorg/jetbrains/exposed/dao/EntityChangeType; + public static final field Removed Lorg/jetbrains/exposed/dao/EntityChangeType; + public static final field Updated Lorg/jetbrains/exposed/dao/EntityChangeType; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/dao/EntityChangeType; + public static fun values ()[Lorg/jetbrains/exposed/dao/EntityChangeType; +} + +public abstract class org/jetbrains/exposed/dao/EntityClass { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun all ()Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun backReferencedOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;)Lkotlin/properties/ReadOnlyProperty; + public final fun backReferencedOnOpt (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;)Lkotlin/properties/ReadOnlyProperty; + public final fun count (Lorg/jetbrains/exposed/sql/Op;)J + public static synthetic fun count$default (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Op;ILjava/lang/Object;)J + protected fun createInstance (Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/sql/ResultRow;)Lorg/jetbrains/exposed/dao/Entity; + public final fun find (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun find (Lorg/jetbrains/exposed/sql/Op;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun findById (Ljava/lang/Comparable;)Lorg/jetbrains/exposed/dao/Entity; + public fun findById (Lorg/jetbrains/exposed/dao/id/EntityID;)Lorg/jetbrains/exposed/dao/Entity; + public final fun findWithCacheCondition (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence; + public fun forEntityIds (Ljava/util/List;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun forIds (Ljava/util/List;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun get (Ljava/lang/Comparable;)Lorg/jetbrains/exposed/dao/Entity; + public final fun get (Lorg/jetbrains/exposed/dao/id/EntityID;)Lorg/jetbrains/exposed/dao/Entity; + public fun getDependsOnColumns ()Ljava/util/List; + public fun getDependsOnTables ()Lorg/jetbrains/exposed/sql/ColumnSet; + public final fun getTable ()Lorg/jetbrains/exposed/dao/id/IdTable; + public final fun isAssignableTo (Lorg/jetbrains/exposed/dao/EntityClass;)Z + public final fun memoizedTransform (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/dao/ColumnWithTransform; + public fun new (Ljava/lang/Comparable;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/dao/Entity; + public fun new (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/dao/Entity; + public final fun optionalBackReferencedOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/OptionalBackReference; + public final fun optionalBackReferencedOnOpt (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/OptionalBackReference; + public final fun optionalReferencedOn (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/OptionalReference; + public final fun optionalReferrersOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/OptionalReferrers; + public final fun optionalReferrersOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;Z)Lorg/jetbrains/exposed/dao/OptionalReferrers; + public static synthetic fun optionalReferrersOn$default (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;ZILjava/lang/Object;)Lorg/jetbrains/exposed/dao/OptionalReferrers; + public final fun referencedOn (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/Reference; + public final fun referrersOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/Referrers; + public final fun referrersOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;Z)Lorg/jetbrains/exposed/dao/Referrers; + public final fun reload (Lorg/jetbrains/exposed/dao/Entity;Z)Lorg/jetbrains/exposed/dao/Entity; + public static synthetic fun reload$default (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/Entity;ZILjava/lang/Object;)Lorg/jetbrains/exposed/dao/Entity; + public final fun removeFromCache (Lorg/jetbrains/exposed/dao/Entity;)V + public fun searchQuery (Lorg/jetbrains/exposed/sql/Op;)Lorg/jetbrains/exposed/sql/Query; + public final fun testCache (Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence; + public final fun testCache (Lorg/jetbrains/exposed/dao/id/EntityID;)Lorg/jetbrains/exposed/dao/Entity; + public final fun transform (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/dao/ColumnWithTransform; + public final fun view (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/dao/View; + protected fun warmCache ()Lorg/jetbrains/exposed/dao/EntityCache; + public final fun warmUpLinkedReferences (Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Boolean;Z)Ljava/util/List; + public static synthetic fun warmUpLinkedReferences$default (Lorg/jetbrains/exposed/dao/EntityClass;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;Ljava/lang/Boolean;ZILjava/lang/Object;)Ljava/util/List; + public final fun warmUpOptReferences (Ljava/util/List;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Boolean;)Ljava/util/List; + public static synthetic fun warmUpOptReferences$default (Lorg/jetbrains/exposed/dao/EntityClass;Ljava/util/List;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Boolean;ILjava/lang/Object;)Ljava/util/List; + public final fun warmUpReferences (Ljava/util/List;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Boolean;)Ljava/util/List; + public static synthetic fun warmUpReferences$default (Lorg/jetbrains/exposed/dao/EntityClass;Ljava/util/List;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Boolean;ILjava/lang/Object;)Ljava/util/List; + public final fun wrap (Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/sql/ResultRow;)Lorg/jetbrains/exposed/dao/Entity; + public final fun wrapRow (Lorg/jetbrains/exposed/sql/ResultRow;)Lorg/jetbrains/exposed/dao/Entity; + public final fun wrapRow (Lorg/jetbrains/exposed/sql/ResultRow;Lorg/jetbrains/exposed/sql/Alias;)Lorg/jetbrains/exposed/dao/Entity; + public final fun wrapRow (Lorg/jetbrains/exposed/sql/ResultRow;Lorg/jetbrains/exposed/sql/QueryAlias;)Lorg/jetbrains/exposed/dao/Entity; + public final fun wrapRows (Lorg/jetbrains/exposed/sql/SizedIterable;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun wrapRows (Lorg/jetbrains/exposed/sql/SizedIterable;Lorg/jetbrains/exposed/sql/Alias;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun wrapRows (Lorg/jetbrains/exposed/sql/SizedIterable;Lorg/jetbrains/exposed/sql/QueryAlias;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/dao/EntityHook { + public static final field INSTANCE Lorg/jetbrains/exposed/dao/EntityHook; + public final fun subscribe (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; + public final fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class org/jetbrains/exposed/dao/EntityHookKt { + public static final fun alertSubscribers (Lorg/jetbrains/exposed/sql/Transaction;)V + public static final fun registerChange (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/dao/EntityChangeType;)V + public static final fun registeredChanges (Lorg/jetbrains/exposed/sql/Transaction;)Ljava/util/List; + public static final fun toEntity (Lorg/jetbrains/exposed/dao/EntityChange;)Lorg/jetbrains/exposed/dao/Entity; + public static final fun toEntity (Lorg/jetbrains/exposed/dao/EntityChange;Lorg/jetbrains/exposed/dao/EntityClass;)Lorg/jetbrains/exposed/dao/Entity; + public static final fun withHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/dao/EntityLifecycleInterceptor : org/jetbrains/exposed/sql/statements/GlobalStatementInterceptor { + public fun ()V + public fun afterCommit (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun afterExecution (Lorg/jetbrains/exposed/sql/Transaction;Ljava/util/List;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public fun afterRollback (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun afterStatementPrepared (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V + public fun beforeCommit (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun beforeExecution (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/statements/StatementContext;)V + public fun beforeRollback (Lorg/jetbrains/exposed/sql/Transaction;)V + public fun keepUserDataInTransactionStoreOnCommit (Ljava/util/Map;)Ljava/util/Map; +} + +public abstract class org/jetbrains/exposed/dao/ImmutableCachedEntityClass : org/jetbrains/exposed/dao/ImmutableEntityClass { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun all ()Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun expireCache ()V + public fun forceUpdateEntity (Lorg/jetbrains/exposed/dao/Entity;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)V + protected final fun warmCache ()Lorg/jetbrains/exposed/dao/EntityCache; +} + +public abstract class org/jetbrains/exposed/dao/ImmutableEntityClass : org/jetbrains/exposed/dao/EntityClass { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun forceUpdateEntity (Lorg/jetbrains/exposed/dao/Entity;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)V +} + +public final class org/jetbrains/exposed/dao/InnerTableLink : kotlin/properties/ReadWriteProperty { + public fun (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/dao/id/IdTable;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getSourceColumn ()Lorg/jetbrains/exposed/sql/Column; + public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; + public final fun getTarget ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getTargetColumn ()Lorg/jetbrains/exposed/sql/Column; + public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable; + public synthetic fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V + public fun setValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Lorg/jetbrains/exposed/sql/SizedIterable;)V +} + +public abstract class org/jetbrains/exposed/dao/IntEntity : org/jetbrains/exposed/dao/Entity { + public fun (Lorg/jetbrains/exposed/dao/id/EntityID;)V +} + +public abstract class org/jetbrains/exposed/dao/IntEntityClass : org/jetbrains/exposed/dao/EntityClass { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract class org/jetbrains/exposed/dao/LongEntity : org/jetbrains/exposed/dao/Entity { + public fun (Lorg/jetbrains/exposed/dao/id/EntityID;)V +} + +public abstract class org/jetbrains/exposed/dao/LongEntityClass : org/jetbrains/exposed/dao/EntityClass { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/dao/OptionalBackReference : kotlin/properties/ReadOnlyProperty { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;)V + public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/dao/Entity; +} + +public final class org/jetbrains/exposed/dao/OptionalReference { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;)V + public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getReference ()Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/dao/OptionalReferrers : kotlin/properties/ReadOnlyProperty { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;Z)V + public final fun getCache ()Z + public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getReference ()Lorg/jetbrains/exposed/sql/Column; + public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/dao/Reference { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;)V + public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getReference ()Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/dao/ReferencesKt { + public static final fun load (Lorg/jetbrains/exposed/dao/Entity;[Lkotlin/reflect/KProperty1;)Lorg/jetbrains/exposed/dao/Entity; + public static final fun with (Ljava/lang/Iterable;[Lkotlin/reflect/KProperty1;)Ljava/lang/Iterable; +} + +public final class org/jetbrains/exposed/dao/Referrers : kotlin/properties/ReadOnlyProperty { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;Z)V + public final fun getCache ()Z + public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getReference ()Lorg/jetbrains/exposed/sql/Column; + public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public abstract class org/jetbrains/exposed/dao/UUIDEntity : org/jetbrains/exposed/dao/Entity { + public fun (Lorg/jetbrains/exposed/dao/id/EntityID;)V +} + +public abstract class org/jetbrains/exposed/dao/UUIDEntityClass : org/jetbrains/exposed/dao/EntityClass { + public fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/dao/id/IdTable;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class org/jetbrains/exposed/dao/View : org/jetbrains/exposed/sql/SizedIterable { + public fun (Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/dao/EntityClass;)V + public fun copy ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun count ()J + public fun empty ()Z + public fun forUpdate (Lorg/jetbrains/exposed/sql/vendors/ForUpdateOption;)Lorg/jetbrains/exposed/sql/SizedIterable; + public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getOp ()Lorg/jetbrains/exposed/sql/Op; + public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun iterator ()Ljava/util/Iterator; + public fun limit (IJ)Lorg/jetbrains/exposed/sql/SizedIterable; + public fun notForUpdate ()Lorg/jetbrains/exposed/sql/SizedIterable; + public fun orderBy ([Lkotlin/Pair;)Lorg/jetbrains/exposed/sql/SizedIterable; +} + +public final class org/jetbrains/exposed/dao/exceptions/EntityNotFoundException : java/lang/Exception { + public fun (Lorg/jetbrains/exposed/dao/id/EntityID;Lorg/jetbrains/exposed/dao/EntityClass;)V + public final fun getEntity ()Lorg/jetbrains/exposed/dao/EntityClass; + public final fun getId ()Lorg/jetbrains/exposed/dao/id/EntityID; +} + diff --git a/exposed-dao/build.gradle.kts b/exposed-dao/build.gradle.kts index bf2454a9cc..9221cccc4e 100644 --- a/exposed-dao/build.gradle.kts +++ b/exposed-dao/build.gradle.kts @@ -6,6 +6,10 @@ repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) } diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt index 41c5486b6e..c789fc1ce9 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt @@ -38,7 +38,10 @@ open class Entity>(val id: EntityID) { internal set val writeValues = LinkedHashMap, Any?>() + + @Suppress("VariableNaming") var _readValues: ResultRow? = null + val readValues: ResultRow get() = _readValues ?: run { val table = klass.table @@ -105,7 +108,7 @@ open class Entity>(val id: EntityID) { else -> { // @formatter:off factory.findWithCacheCondition({ - reference.referee!!.getValue(this, desc) == refValue + reference.referee!!.getValue(this, desc) == refValue }) { reference.referee()!! eq refValue }.singleOrNull()?.also { @@ -146,13 +149,13 @@ open class Entity>(val id: EntityID) { } else -> { // @formatter:off - factory.findWithCacheCondition({ - reference.referee!!.getValue(this, desc) == refValue - }) { - reference.referee()!! eq refValue - }.singleOrNull().also { - storeReferenceInCache(reference, it) - } + factory.findWithCacheCondition( + { reference.referee!!.getValue(this, desc) == refValue } + ) { + reference.referee()!! eq refValue + }.singleOrNull().also { + storeReferenceInCache(reference, it) + } // @formatter:on } } @@ -178,7 +181,6 @@ open class Entity>(val id: EntityID) { return this.restoreValueFromParts(values) } - @Suppress("UNCHECKED_CAST") fun Column.lookupInReadValues(found: (T?) -> R?, notFound: () -> R?): R? = if (_readValues?.hasValue(this) == true) { found(readValues[this]) @@ -261,6 +263,8 @@ open class Entity>(val id: EntityID) { if (batch == null) { val table = klass.table // Store values before update to prevent flush inside UpdateStatement + + @Suppress("VariableNaming") val _writeValues = writeValues.toMap() storeWrittenValues() // In case of batch all changes will be registered after all entities flushed diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityCache.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityCache.kt index 3183c27a8e..8efcc0e1f0 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityCache.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityCache.kt @@ -14,7 +14,7 @@ val Transaction.entityCache: EntityCache by transactionScope { EntityCache(this) class EntityCache(private val transaction: Transaction) { private var flushingEntities = false private var initializingEntities: LinkedIdentityHashSet> = LinkedIdentityHashSet() - internal val pendingInitializationLambdas = IdentityHashMap, MutableList<(Entity<*>)->Unit>>() + internal val pendingInitializationLambdas = IdentityHashMap, MutableList<(Entity<*>) -> Unit>>() val data = LinkedHashMap, MutableMap>>() internal val inserts = LinkedHashMap, MutableSet>>() private val updates = LinkedHashMap, MutableSet>>() @@ -24,7 +24,6 @@ class EntityCache(private val transaction: Transaction) { * Amount of entities to keep in a cache per an Entity class. * On setting a new value all data stored in cache will be adjusted to a new size */ - @Suppress("MemberVisibilityCanBePrivate") var maxEntitiesToStore = transaction.db.config.maxEntitiesToStoreInCachePerEntity set(value) { val diff = value - field diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt index da76cb5b05..dc12d40c33 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt @@ -294,46 +294,136 @@ abstract class EntityClass, out T : Entity>( private inline fun registerRefRule(column: Column<*>, ref: () -> R): R = refDefinitions.getOrPut(column to R::class, ref) as R + /** + * Registers a reference as a field of the child entity class, which returns a parent object of this `EntityClass`. + * + * The reference should have been defined by the creation of a [column] using `reference()` on the child table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Child + */ infix fun > referencedOn(column: Column) = registerRefRule(column) { Reference(column, this) } + /** + * Registers an optional reference as a field of the child entity class, which returns a parent object of + * this `EntityClass`. + * + * The reference should have been defined by the creation of a [column] using either `optReference()` or + * `reference().nullable()` on the child table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Post + */ infix fun > optionalReferencedOn(column: Column) = registerRefRule(column) { OptionalReference(column, this) } + /** + * Registers a reference as an immutable field of the parent entity class, which returns a child object of + * this `EntityClass`. + * + * The reference should have been defined by the creation of a [column] using `reference()` on the child table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.YEntity + */ infix fun , Target : Entity, REF : Comparable> EntityClass.backReferencedOn( column: Column - ): - ReadOnlyProperty, Target> = registerRefRule(column) { BackReference(column, this) } + ): ReadOnlyProperty, Target> = registerRefRule(column) { BackReference(column, this) } + /** + * Registers a reference as an immutable field of the parent entity class, which returns a child object of + * this `EntityClass`. + * + * The reference should have been defined by the creation of a [column] using `reference()` on the child table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.YEntity + */ @JvmName("backReferencedOnOpt") infix fun , Target : Entity, REF : Comparable> EntityClass.backReferencedOn( column: Column - ): - ReadOnlyProperty, Target> = registerRefRule(column) { BackReference(column, this) } + ): ReadOnlyProperty, Target> = registerRefRule(column) { BackReference(column, this) } + /** + * Registers an optional reference as an immutable field of the parent entity class, which returns a child object of + * this `EntityClass`. + * + * The reference should have been defined by the creation of a [column] using either `optReference()` or + * `reference().nullable()` on the child table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student + */ infix fun , Target : Entity, REF : Comparable> EntityClass.optionalBackReferencedOn( column: Column ) = registerRefRule(column) { OptionalBackReference, REF>(column as Column, this) } + /** + * Registers an optional reference as an immutable field of the parent entity class, which returns a child object of + * this `EntityClass`. + * + * The reference should have been defined by the creation of a [column] using either `optReference()` or + * `reference().nullable()` on the child table. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student + */ @JvmName("optionalBackReferencedOnOpt") infix fun , Target : Entity, REF : Comparable> EntityClass.optionalBackReferencedOn( column: Column ) = registerRefRule(column) { OptionalBackReference, REF>(column, this) } + /** + * Registers a reference as an immutable field of the parent entity class, which returns a collection of + * child objects of this `EntityClass` that all reference the parent. + * + * The reference should have been defined by the creation of a [column] using `reference()` on the child table. + * + * By default, this also stores the loaded entities to a cache. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.Country + */ infix fun , Target : Entity, REF : Comparable> EntityClass.referrersOn(column: Column) = registerRefRule(column) { Referrers, TargetID, Target, REF>(column, this, true) } + /** + * Registers a reference as an immutable field of the parent entity class, which returns a collection of + * child objects of this `EntityClass` that all reference the parent. + * + * The reference should have been defined by the creation of a [column] using `reference()` on the child table. + * + * Set [cache] to `true` to also store the loaded entities to a cache. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.School + */ fun , Target : Entity, REF : Comparable> EntityClass.referrersOn( column: Column, cache: Boolean ) = registerRefRule(column) { Referrers, TargetID, Target, REF>(column, this, cache) } + /** + * Registers an optional reference as an immutable field of the parent entity class, which returns a collection of + * child objects of this `EntityClass` that all reference the parent. + * + * The reference should have been defined by the creation of a [column] using either `optReference()` or + * reference().nullable()` on the child table. + * + * By default, this also stores the loaded entities to a cache. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Category + */ infix fun , Target : Entity, REF : Comparable> EntityClass.optionalReferrersOn( column: Column ) = registerRefRule(column) { OptionalReferrers, TargetID, Target, REF>(column, this, true) } + /** + * Registers an optional reference as an immutable field of the parent entity class, which returns a collection of + * child objects of this `EntityClass` that all reference the parent. + * + * The reference should have been defined by the creation of a [column] using either `optReference()` or + * `reference().nullable()` on the child table. + * + * Set [cache] to `true` to also store the loaded entities to a cache. + * + * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student + */ fun , Target : Entity, REF : Comparable> EntityClass.optionalReferrersOn( column: Column, cache: Boolean = false @@ -356,7 +446,6 @@ abstract class EntityClass, out T : Entity>( private fun Query.setForUpdateStatus(): Query = if (this@EntityClass is ImmutableEntityClass<*, *>) this.notForUpdate() else this - @Suppress("CAST_NEVER_SUCCEEDS") fun warmUpOptReferences(references: List, refColumn: Column, forUpdate: Boolean? = null): List = warmUpReferences(references, refColumn as Column, forUpdate) @@ -419,10 +508,15 @@ abstract class EntityClass, out T : Entity>( else -> findQuery }.toList() - internal fun > warmUpLinkedReferences( - references: List>, sourceRefColumn: Column>, targetRefColumn: Column>, linkTable: Table, - forUpdate: Boolean? = null, optimizedLoad: Boolean = false - ) : List { + @Suppress("ComplexMethod") + internal fun > warmUpLinkedReferences( + references: List>, + sourceRefColumn: Column>, + targetRefColumn: Column>, + linkTable: Table, + forUpdate: Boolean? = null, + optimizedLoad: Boolean = false + ): List { if (references.isEmpty()) return emptyList() val distinctRefIds = references.distinct() val transaction = TransactionManager.current() @@ -475,7 +569,12 @@ abstract class EntityClass, out T : Entity>( * Can be useful when references target the same entities. That will prevent from loading them multiple times (per each reference row) and will require * less memory/bandwidth for "heavy" entities (with a lot of columns or columns with huge data in it) */ - fun > warmUpLinkedReferences(references: List>, linkTable: Table, forUpdate: Boolean? = null, optimizedLoad: Boolean = false): List { + fun > warmUpLinkedReferences( + references: List>, + linkTable: Table, + forUpdate: Boolean? = null, + optimizedLoad: Boolean = false + ): List { if (references.isEmpty()) return emptyList() val sourceRefColumn = linkTable.columns.singleOrNull { it.referee == references.first().table.id } as? Column> @@ -489,8 +588,6 @@ abstract class EntityClass, out T : Entity>( fun , T : Entity> isAssignableTo(entityClass: EntityClass) = entityClass.klass.isAssignableFrom(klass) } - - abstract class ImmutableEntityClass, out T : Entity>( table: IdTable, entityType: Class? = null, @@ -528,19 +625,21 @@ abstract class ImmutableCachedEntityClass, out T : Entity { - } // already loaded in another transaction - tr.getUserData(cacheLoadingState) != null -> { - return transactionCache // prevent recursive call to warmCache() in .all() - } - else -> { - tr.putUserData(cacheLoadingState, this) - super.all().toList() /* force iteration to initialize lazy collection */ - _cachedValues[db] = transactionCache.data[table] ?: mutableMapOf() - tr.removeUserData(cacheLoadingState) + if (_cachedValues[db] == null) { + synchronized(this) { + val cachedValues = _cachedValues[db] + when { + cachedValues != null -> { + } // already loaded in another transaction + tr.getUserData(cacheLoadingState) != null -> { + return transactionCache // prevent recursive call to warmCache() in .all() + } + else -> { + tr.putUserData(cacheLoadingState, this) + super.all().toList() // force iteration to initialize lazy collection + _cachedValues[db] = transactionCache.data[table] ?: mutableMapOf() + tr.removeUserData(cacheLoadingState) + } } } } diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityHook.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityHook.kt index af1ad902bc..26f8b16f1b 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityHook.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityHook.kt @@ -11,7 +11,7 @@ import java.util.concurrent.ConcurrentLinkedQueue enum class EntityChangeType { Created, Updated, - Removed; + Removed } data class EntityChange( @@ -25,7 +25,6 @@ fun , T : Entity> EntityChange.toEntity(): T? = (entityC fun , T : Entity> EntityChange.toEntity(klass: EntityClass): T? { if (!entityClass.isAssignableTo(klass)) return null - @Suppress("UNCHECKED_CAST") return toEntity() } @@ -53,7 +52,6 @@ fun Transaction.registerChange(entityClass: EntityClass<*, Entity<*>>, entityId: } } - private var isProcessingEventsLaunched by transactionScope { false } fun Transaction.alertSubscribers() { if (isProcessingEventsLaunched) return diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityLifecycleInterceptor.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityLifecycleInterceptor.kt index 028727b6d5..00e8593ee4 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityLifecycleInterceptor.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityLifecycleInterceptor.kt @@ -27,6 +27,7 @@ class EntityLifecycleInterceptor : GlobalStatementInterceptor { return userData.filterValues { it is EntityCache } } + @Suppress("ComplexMethod") override fun beforeExecution(transaction: Transaction, context: StatementContext) { when (val statement = context.statement) { is Query -> transaction.flushEntities(statement) @@ -76,8 +77,9 @@ class EntityLifecycleInterceptor : GlobalStatementInterceptor { } override fun afterExecution(transaction: Transaction, contexts: List, executedStatement: PreparedStatementApi) { - if (!isExecutedWithinEntityLifecycle || contexts.first().statement !is InsertStatement<*>) + if (!isExecutedWithinEntityLifecycle || contexts.first().statement !is InsertStatement<*>) { transaction.alertSubscribers() + } } override fun beforeCommit(transaction: Transaction) { diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/IntEntity.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/IntEntity.kt index 8ec2f9f1a2..ba1524a179 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/IntEntity.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/IntEntity.kt @@ -3,8 +3,10 @@ package org.jetbrains.exposed.dao import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable +@Suppress("UnnecessaryAbstractClass") abstract class IntEntity(id: EntityID) : Entity(id) +@Suppress("UnnecessaryAbstractClass") abstract class IntEntityClass constructor( table: IdTable, entityType: Class? = null, diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/LongEntity.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/LongEntity.kt index ac228eb047..94d0388ea4 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/LongEntity.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/LongEntity.kt @@ -3,8 +3,10 @@ package org.jetbrains.exposed.dao import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable +@Suppress("UnnecessaryAbstractClass") abstract class LongEntity(id: EntityID) : Entity(id) +@Suppress("UnnecessaryAbstractClass") abstract class LongEntityClass( table: IdTable, entityType: Class? = null, diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/References.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/References.kt index 72a9b44159..74c0921a80 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/References.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/References.kt @@ -37,24 +37,31 @@ class OptionalReference, ID : Comparable, out Target : } } -internal class BackReference, out Parent : Entity, ChildID : Comparable, in Child : Entity, REF> - (reference: Column, factory: EntityClass) : ReadOnlyProperty { +internal class BackReference, out Parent : Entity, ChildID : Comparable, in Child : Entity, REF>( + reference: Column, + factory: EntityClass +) : ReadOnlyProperty { internal val delegate = Referrers(reference, factory, true) override operator fun getValue(thisRef: Child, property: KProperty<*>) = delegate.getValue(thisRef.apply { thisRef.id.value }, property).single() // flush entity before to don't miss newly created entities } -class OptionalBackReference, out Parent : Entity, ChildID : Comparable, in Child : Entity, REF> - (reference: Column, factory: EntityClass) : ReadOnlyProperty { +class OptionalBackReference, out Parent : Entity, ChildID : Comparable, in Child : Entity, REF>( + reference: Column, + factory: EntityClass +) : ReadOnlyProperty { internal val delegate = OptionalReferrers(reference, factory, true) override operator fun getValue(thisRef: Child, property: KProperty<*>) = delegate.getValue(thisRef.apply { thisRef.id.value }, property).singleOrNull() // flush entity before to don't miss newly created entities } -class Referrers, in Parent : Entity, ChildID : Comparable, out Child : Entity, REF> - (val reference: Column, val factory: EntityClass, val cache: Boolean) : ReadOnlyProperty> { +class Referrers, in Parent : Entity, ChildID : Comparable, out Child : Entity, REF>( + val reference: Column, + val factory: EntityClass, + val cache: Boolean +) : ReadOnlyProperty> { init { reference.referee ?: error("Column $reference is not a reference") @@ -81,8 +88,11 @@ class Referrers, in Parent : Entity, C } } -class OptionalReferrers, in Parent : Entity, ChildID : Comparable, out Child : Entity, REF> - (val reference: Column, val factory: EntityClass, val cache: Boolean) : ReadOnlyProperty> { +class OptionalReferrers, in Parent : Entity, ChildID : Comparable, out Child : Entity, REF>( + val reference: Column, + val factory: EntityClass, + val cache: Boolean +) : ReadOnlyProperty> { init { reference.referee ?: error("Column $reference is not a reference") @@ -122,7 +132,7 @@ private fun > filterRelationsForEntity( return validMembers.filter { it in relations } as Collection> } -@Suppress("UNCHECKED_CAST") +@Suppress("UNCHECKED_CAST", "NestedBlockDepth", "ComplexMethod") private fun > List>.preloadRelations( vararg relations: KProperty1, Any?>, nodesVisited: MutableSet> = mutableSetOf() @@ -228,7 +238,7 @@ private fun > List>.preloadRelations( } } -fun , SRC : Entity, REF : Entity<*>, L: Iterable> L.with(vararg relations: KProperty1): L { +fun , SRC : Entity, REF : Entity<*>, L : Iterable> L.with(vararg relations: KProperty1): L { toList().apply { if (any { it.isNewEntity() }) { TransactionManager.current().flushCache() diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/UUIDEntity.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/UUIDEntity.kt index 8c9bc83b11..4d4696f49b 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/UUIDEntity.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/UUIDEntity.kt @@ -4,8 +4,10 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable import java.util.* +@Suppress("UnnecessaryAbstractClass") abstract class UUIDEntity(id: EntityID) : Entity(id) +@Suppress("UnnecessaryAbstractClass") abstract class UUIDEntityClass( table: IdTable, entityType: Class? = null, diff --git a/exposed-java-time/api/exposed-java-time.api b/exposed-java-time/api/exposed-java-time.api new file mode 100644 index 0000000000..973e540dc6 --- /dev/null +++ b/exposed-java-time/api/exposed-java-time.api @@ -0,0 +1,184 @@ +public final class org/jetbrains/exposed/sql/javatime/CurrentDate : org/jetbrains/exposed/sql/Function { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/javatime/CurrentDate; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/CurrentDateTime : org/jetbrains/exposed/sql/Function { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/javatime/CurrentDateTime; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/CurrentTimestamp : org/jetbrains/exposed/sql/Function { + public fun ()V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Date : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Day : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Hour : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/JavaDateColumnTypeKt { + public static final fun date (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun datetime (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun duration (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun time (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun timestamp (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun timestampWithTimeZone (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaDateFunctionsKt { + public static final fun CustomDateFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomDateTimeFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomDurationFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimeFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimeStampFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimestampWithTimeZoneFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun date (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Date; + public static final fun dateLiteral (Ljava/time/LocalDate;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun dateParam (Ljava/time/LocalDate;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun dateTimeLiteral (Ljava/time/LocalDateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun dateTimeParam (Ljava/time/LocalDateTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun day (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Day; + public static final fun durationLiteral (Ljava/time/Duration;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun durationParam (Ljava/time/Duration;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun hour (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Hour; + public static final fun minute (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Minute; + public static final fun month (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Month; + public static final fun second (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Second; + public static final fun timeLiteral (Ljava/time/LocalTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timeParam (Ljava/time/LocalTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun timestampLiteral (Ljava/time/Instant;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timestampParam (Ljava/time/Instant;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun timestampWithTimeZoneLiteral (Ljava/time/OffsetDateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timestampWithTimeZoneParam (Ljava/time/OffsetDateTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun year (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/javatime/Year; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaDurationColumnType : org/jetbrains/exposed/sql/ColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/javatime/JavaDurationColumnType$Companion; + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/time/Duration; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaDurationColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/javatime/JavaInstantColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/javatime/JavaInstantColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/time/Instant; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaInstantColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/javatime/JavaLocalDateColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/javatime/JavaLocalDateColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaLocalDateColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/javatime/JavaLocalDateTimeColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/javatime/JavaLocalDateTimeColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaLocalDateTimeColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/javatime/JavaLocalTimeColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/javatime/JavaLocalTimeColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/time/LocalTime; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaLocalTimeColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/javatime/JavaOffsetDateTimeColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/javatime/JavaOffsetDateTimeColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/time/OffsetDateTime; +} + +public final class org/jetbrains/exposed/sql/javatime/JavaOffsetDateTimeColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/javatime/Minute : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Month : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Second : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Time : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/javatime/Year : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + diff --git a/exposed-java-time/build.gradle.kts b/exposed-java-time/build.gradle.kts index 457adc6faf..a8e1ecc6d1 100644 --- a/exposed-java-time/build.gradle.kts +++ b/exposed-java-time/build.gradle.kts @@ -3,29 +3,30 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { kotlin("jvm") apply true - id("testWithDBs") + kotlin("plugin.serialization") apply true } repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) testImplementation(project(":exposed-dao")) testImplementation(project(":exposed-tests")) + testImplementation(project(":exposed-json")) testImplementation("junit", "junit", "4.12") testImplementation(kotlin("test-junit")) } -//tasks.withType().configureEach { -// // Target version of the generated JVM bytecode. It is used for type resolution. -// jvmTarget = "1.8" -//} - 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-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt index dfea6d7fec..dad1131da9 100644 --- a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt +++ b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateColumnType.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.sql.ColumnType import org.jetbrains.exposed.sql.IDateColumnType import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.OracleDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect import org.jetbrains.exposed.sql.vendors.currentDialect @@ -38,6 +39,34 @@ internal val DEFAULT_TIME_STRING_FORMATTER by lazy { DateTimeFormatter.ISO_LOCAL_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault()) } +// Example result: 2023-07-07 14:42:29.343+02:00 or 2023-07-07 12:42:29.343Z +internal val SQLITE_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSS[XXX]", + Locale.ROOT + ) +} + +// For UTC time zone, MySQL rejects the 'Z' and will only accept the offset '+00:00' +internal val MYSQL_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSSSSS[xxx]", + Locale.ROOT + ) +} + +// Example result: 2023-07-07 14:42:29.343789 +02:00 +internal val ORACLE_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSSSSS [xxx]", + Locale.ROOT + ) +} + +internal val DEFAULT_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(Locale.ROOT) +} + internal fun formatterForDateString(date: String) = dateTimeWithFractionFormat(date.substringAfterLast('.', "").length) internal fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter { val baseFormat = "yyyy-MM-d HH:mm:ss" @@ -49,8 +78,10 @@ internal fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter { return DateTimeFormatter.ofPattern(newFormat).withLocale(Locale.ROOT).withZone(ZoneId.systemDefault()) } +@Suppress("MagicNumber") internal val LocalDate.millis get() = atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000 +@Suppress("MagicNumber") class JavaLocalDateColumnType : ColumnType(), IDateColumnType { override val hasTimePart: Boolean = false @@ -94,6 +125,7 @@ class JavaLocalDateColumnType : ColumnType(), IDateColumnType { } } +@Suppress("MagicNumber") class JavaLocalDateTimeColumnType : ColumnType(), IDateColumnType { override val hasTimePart: Boolean = true override fun sqlType(): String = currentDialect.dataTypeProvider.dateTimeType() @@ -238,6 +270,56 @@ class JavaInstantColumnType : ColumnType(), IDateColumnType { } } +class JavaOffsetDateTimeColumnType : ColumnType(), IDateColumnType { + override val hasTimePart: Boolean = true + + override fun sqlType(): String = currentDialect.dataTypeProvider.timestampWithTimeZoneType() + + override fun nonNullValueToString(value: Any): String = when (value) { + is OffsetDateTime -> { + when (currentDialect) { + is SQLiteDialect -> "'${value.format(SQLITE_OFFSET_DATE_TIME_FORMATTER)}'" + is MysqlDialect -> "'${value.format(MYSQL_OFFSET_DATE_TIME_FORMATTER)}'" + is OracleDialect -> "'${value.format(ORACLE_OFFSET_DATE_TIME_FORMATTER)}'" + else -> "'${value.format(DEFAULT_OFFSET_DATE_TIME_FORMATTER)}'" + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + override fun valueFromDB(value: Any): OffsetDateTime = when (value) { + is OffsetDateTime -> value + is String -> { + if (currentDialect is SQLiteDialect) { + OffsetDateTime.parse(value, SQLITE_OFFSET_DATE_TIME_FORMATTER) + } else { + OffsetDateTime.parse(value) + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + override fun readObject(rs: ResultSet, index: Int): Any? = when (currentDialect) { + is SQLiteDialect -> super.readObject(rs, index) + else -> rs.getObject(index, OffsetDateTime::class.java) + } + + override fun notNullValueToDB(value: Any): Any = when (value) { + is OffsetDateTime -> { + when (currentDialect) { + is SQLiteDialect -> value.format(SQLITE_OFFSET_DATE_TIME_FORMATTER) + is MysqlDialect -> value.format(MYSQL_OFFSET_DATE_TIME_FORMATTER) + else -> value + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + companion object { + internal val INSTANCE = JavaOffsetDateTimeColumnType() + } +} + class JavaDurationColumnType : ColumnType() { override fun sqlType(): String = currentDialect.dataTypeProvider.longType() @@ -285,7 +367,7 @@ class JavaDurationColumnType : ColumnType() { fun Table.date(name: String): Column = registerColumn(name, JavaLocalDateColumnType()) /** - * A datetime column to store both a date and a time. + * A datetime column to store both a date and a time without time zone. * * @param name The column name */ @@ -302,12 +384,23 @@ fun Table.datetime(name: String): Column = registerColumn(name, J fun Table.time(name: String): Column = registerColumn(name, JavaLocalTimeColumnType()) /** - * A timestamp column to store both a date and a time. + * A timestamp column to store both a date and a time without time zone. * * @param name The column name */ fun Table.timestamp(name: String): Column = registerColumn(name, JavaInstantColumnType()) +/** + * A timestamp column to store both a date and a time with time zone. + * + * Note: PostgreSQL and MySQL always store the timestamp in UTC, thereby losing the original time zone. To preserve the + * original time zone, store the time zone information in a separate column. + * + * @param name The column name + */ +fun Table.timestampWithTimeZone(name: String): Column = + registerColumn(name, JavaOffsetDateTimeColumnType()) + /** * A date column to store a duration. * diff --git a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt index 60bd11c081..cb1aad4ac4 100644 --- a/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt +++ b/exposed-java-time/src/main/kotlin/org/jetbrains/exposed/sql/javatime/JavaDateFunctions.kt @@ -12,6 +12,7 @@ import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.time.OffsetDateTime import java.time.temporal.Temporal class Date(val expr: Expression) : Function(JavaLocalDateColumnType.INSTANCE) { @@ -29,14 +30,6 @@ object CurrentDateTime : Function(JavaLocalDateTimeColumnType.INS else -> "CURRENT_TIMESTAMP" } } - - @Deprecated( - message = "This class is now a singleton, no need for its constructor call; " + - "this method is provided for backward-compatibility only, and will be removed in future releases", - replaceWith = ReplaceWith("this"), - level = DeprecationLevel.ERROR, - ) - operator fun invoke() = this } object CurrentDate : Function(JavaLocalDateColumnType.INSTANCE) { @@ -49,7 +42,7 @@ object CurrentDate : Function(JavaLocalDateColumnType.INSTANCE) { } } -class CurrentTimestamp : Expression() { +class CurrentTimestamp : Function(JavaInstantColumnType.INSTANCE) { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { +when { (currentDialect as? MysqlDialect)?.isFractionDateTimeSupported() == true -> "CURRENT_TIMESTAMP(6)" @@ -139,6 +132,10 @@ fun dateTimeParam(value: LocalDateTime): Expression = QueryParameter(value, JavaLocalDateTimeColumnType.INSTANCE) fun timestampParam(value: Instant): Expression = QueryParameter(value, JavaInstantColumnType.INSTANCE) + +fun timestampWithTimeZoneParam(value: OffsetDateTime): Expression = + QueryParameter(value, JavaOffsetDateTimeColumnType.INSTANCE) + fun durationParam(value: Duration): Expression = QueryParameter(value, JavaDurationColumnType.INSTANCE) fun dateLiteral(value: LocalDate): LiteralOp = LiteralOp(JavaLocalDateColumnType.INSTANCE, value) @@ -146,6 +143,8 @@ fun timeLiteral(value: LocalTime): LiteralOp = LiteralOp(JavaLocalTim fun dateTimeLiteral(value: LocalDateTime): LiteralOp = LiteralOp(JavaLocalDateTimeColumnType.INSTANCE, value) fun timestampLiteral(value: Instant): LiteralOp = LiteralOp(JavaInstantColumnType.INSTANCE, value) +fun timestampWithTimeZoneLiteral(value: OffsetDateTime): LiteralOp = + LiteralOp(JavaOffsetDateTimeColumnType.INSTANCE, value) fun durationLiteral(value: Duration): LiteralOp = LiteralOp(JavaDurationColumnType.INSTANCE, value) @Suppress("FunctionName") @@ -164,6 +163,12 @@ fun CustomDateTimeFunction(functionName: String, vararg params: Expression<*>): fun CustomTimeStampFunction(functionName: String, vararg params: Expression<*>): CustomFunction = CustomFunction(functionName, JavaInstantColumnType.INSTANCE, *params) +@Suppress("FunctionName") +fun CustomTimestampWithTimeZoneFunction( + functionName: String, + vararg params: Expression<*> +): CustomFunction = CustomFunction(functionName, JavaOffsetDateTimeColumnType.INSTANCE, *params) + @Suppress("FunctionName") fun CustomDurationFunction(functionName: String, vararg params: Expression<*>): CustomFunction = CustomFunction(functionName, JavaDurationColumnType.INSTANCE, *params) diff --git a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt index 334ff3b8d9..0c9f1906d9 100644 --- a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt +++ b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/DefaultsTest.kt @@ -11,11 +11,13 @@ import org.jetbrains.exposed.sql.statements.BatchDataInconsistentException import org.jetbrains.exposed.sql.statements.BatchInsertStatement import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.constraintNamePart import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.inProperCase import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.H2Dialect import org.jetbrains.exposed.sql.vendors.MysqlDialect @@ -213,7 +215,7 @@ class DefaultsTest : DatabaseTestsBase() { val tmConstValue = LocalTime.of(12, 0) val tLiteral = timeLiteral(tmConstValue) - val TestTable = object : IntIdTable("t") { + val testTable = object : IntIdTable("t") { val s = varchar("s", 100).default("test") val sn = varchar("sn", 100).default("testNullable").nullable() val l = long("l").default(42) @@ -236,7 +238,7 @@ class DefaultsTest : DatabaseTestsBase() { else -> "NULL" } - withTables(listOf(TestDB.SQLITE), TestTable) { + withTables(listOf(TestDB.SQLITE), testTable) { val dtType = currentDialectTest.dataTypeProvider.dateTimeType() val longType = currentDialectTest.dataTypeProvider.longType() val timeType = currentDialectTest.dataTypeProvider.timeType() @@ -245,20 +247,20 @@ class DefaultsTest : DatabaseTestsBase() { val baseExpression = "CREATE TABLE " + addIfNotExistsIfSupported() + "${"t".inProperCase()} (" + "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerAutoincType()} PRIMARY KEY, " + - "${"s".inProperCase()} $varcharType DEFAULT 'test' NOT NULL, " + - "${"sn".inProperCase()} $varcharType DEFAULT 'testNullable' NULL, " + - "${"l".inProperCase()} ${currentDialectTest.dataTypeProvider.longType()} DEFAULT 42 NOT NULL, " + - "$q${"c".inProperCase()}$q CHAR DEFAULT 'X' NOT NULL, " + - "${"t1".inProperCase()} $dtType ${currentDT.itOrNull()}, " + - "${"t2".inProperCase()} $dtType ${nowExpression.itOrNull()}, " + - "${"t3".inProperCase()} $dtType ${dtLiteral.itOrNull()}, " + - "${"t4".inProperCase()} DATE ${dLiteral.itOrNull()}, " + - "${"t5".inProperCase()} $dtType ${tsLiteral.itOrNull()}, " + - "${"t6".inProperCase()} $dtType ${tsLiteral.itOrNull()}, " + - "${"t7".inProperCase()} $longType ${durLiteral.itOrNull()}, " + - "${"t8".inProperCase()} $longType ${durLiteral.itOrNull()}, " + - "${"t9".inProperCase()} $timeType ${tLiteral.itOrNull()}, " + - "${"t10".inProperCase()} $timeType ${tLiteral.itOrNull()}" + + "${"s".inProperCase()} $varcharType${testTable.s.constraintNamePart()} DEFAULT 'test' NOT NULL, " + + "${"sn".inProperCase()} $varcharType${testTable.sn.constraintNamePart()} DEFAULT 'testNullable' NULL, " + + "${"l".inProperCase()} ${currentDialectTest.dataTypeProvider.longType()}${testTable.l.constraintNamePart()} DEFAULT 42 NOT NULL, " + + "$q${"c".inProperCase()}$q CHAR${testTable.c.constraintNamePart()} DEFAULT 'X' NOT NULL, " + + "${"t1".inProperCase()} $dtType${testTable.t1.constraintNamePart()} ${currentDT.itOrNull()}, " + + "${"t2".inProperCase()} $dtType${testTable.t2.constraintNamePart()} ${nowExpression.itOrNull()}, " + + "${"t3".inProperCase()} $dtType${testTable.t3.constraintNamePart()} ${dtLiteral.itOrNull()}, " + + "${"t4".inProperCase()} DATE${testTable.t4.constraintNamePart()} ${dLiteral.itOrNull()}, " + + "${"t5".inProperCase()} $dtType${testTable.t5.constraintNamePart()} ${tsLiteral.itOrNull()}, " + + "${"t6".inProperCase()} $dtType${testTable.t6.constraintNamePart()} ${tsLiteral.itOrNull()}, " + + "${"t7".inProperCase()} $longType${testTable.t7.constraintNamePart()} ${durLiteral.itOrNull()}, " + + "${"t8".inProperCase()} $longType${testTable.t8.constraintNamePart()} ${durLiteral.itOrNull()}, " + + "${"t9".inProperCase()} $timeType${testTable.t9.constraintNamePart()} ${tLiteral.itOrNull()}, " + + "${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" + ")" val expected = if (currentDialectTest is OracleDialect || currentDialectTest.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { @@ -267,23 +269,23 @@ class DefaultsTest : DatabaseTestsBase() { arrayListOf(baseExpression) } - assertEqualLists(expected, TestTable.ddl) - - val id1 = TestTable.insertAndGetId { } - - val row1 = TestTable.select { TestTable.id eq id1 }.single() - assertEquals("test", row1[TestTable.s]) - assertEquals("testNullable", row1[TestTable.sn]) - assertEquals(42, row1[TestTable.l]) - assertEquals('X', row1[TestTable.c]) - assertEqualDateTime(dtConstValue.atStartOfDay(), row1[TestTable.t3]) - assertEqualDateTime(dtConstValue, row1[TestTable.t4]) - assertEqualDateTime(tsConstValue, row1[TestTable.t5]) - assertEqualDateTime(tsConstValue, row1[TestTable.t6]) - assertEquals(durConstValue, row1[TestTable.t7]) - assertEquals(durConstValue, row1[TestTable.t8]) - assertEquals(tmConstValue, row1[TestTable.t9]) - assertEquals(tmConstValue, row1[TestTable.t10]) + assertEqualLists(expected, testTable.ddl) + + val id1 = testTable.insertAndGetId { } + + val row1 = testTable.select { testTable.id eq id1 }.single() + assertEquals("test", row1[testTable.s]) + assertEquals("testNullable", row1[testTable.sn]) + assertEquals(42, row1[testTable.l]) + assertEquals('X', row1[testTable.c]) + assertEqualDateTime(dtConstValue.atStartOfDay(), row1[testTable.t3]) + assertEqualDateTime(dtConstValue, row1[testTable.t4]) + assertEqualDateTime(tsConstValue, row1[testTable.t5]) + assertEqualDateTime(tsConstValue, row1[testTable.t6]) + assertEquals(durConstValue, row1[testTable.t7]) + assertEquals(durConstValue, row1[testTable.t8]) + assertEquals(tmConstValue, row1[testTable.t9]) + assertEquals(tmConstValue, row1[testTable.t10]) } } @@ -364,7 +366,10 @@ class DefaultsTest : DatabaseTestsBase() { fun testConsistentSchemeWithFunctionAsDefaultExpression() { val foo = object : IntIdTable("foo") { val name = text("name") - val defaultDateTime = datetime("defaultDateTime").defaultExpression(CurrentDateTime) + val defaultDate = date("default_date").defaultExpression(CurrentDate) + val defaultDateTime1 = datetime("default_date_time_1").defaultExpression(CurrentDateTime) + val defaultDateTime2 = datetime("default_date_time_2").defaultExpression(CurrentTimestamp()) + val defaultTimeStamp = timestamp("default_time_stamp").defaultExpression(CurrentTimestamp()) } withDb { @@ -372,10 +377,75 @@ class DefaultsTest : DatabaseTestsBase() { SchemaUtils.create(foo) val actual = SchemaUtils.statementsRequiredToActualizeScheme(foo) - assertTrue(actual.isEmpty()) + + if (currentDialectTest is MysqlDialect) { + // MySQL and MariaDB do not support CURRENT_DATE as default + // so the column is created with a NULL marker, which correctly triggers 1 alter statement + val tableName = foo.nameInDatabaseCase() + val dateColumnName = foo.defaultDate.nameInDatabaseCase() + val alter = "ALTER TABLE $tableName MODIFY COLUMN $dateColumnName DATE NULL" + assertEquals(alter, actual.single()) + } else { + assertTrue(actual.isEmpty()) + } } finally { SchemaUtils.drop(foo) } } } + + @Test + fun testTimestampWithTimeZoneDefaults() { + // UTC time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC)) + assertEquals("UTC", ZoneId.systemDefault().id) + + val nowWithTimeZone = OffsetDateTime.now() + val timestampWithTimeZoneLiteral = timestampWithTimeZoneLiteral(nowWithTimeZone) + + val testTable = object : IntIdTable("t") { + val t1 = timestampWithTimeZone("t1").default(nowWithTimeZone) + val t2 = timestampWithTimeZone("t2").defaultExpression(timestampWithTimeZoneLiteral) + } + + fun Expression<*>.itOrNull() = when { + currentDialectTest.isAllowedAsColumnDefault(this) -> + "DEFAULT ${currentDialectTest.dataTypeProvider.processForDefaultValue(this)} NOT NULL" + else -> "NULL" + } + + withDb(excludeSettings = listOf(TestDB.SQLITE, TestDB.MARIADB)) { + if (!isOldMySql()) { + SchemaUtils.create(testTable) + + val timestampWithTimeZoneType = currentDialectTest.dataTypeProvider.timestampWithTimeZoneType() + + val baseExpression = "CREATE TABLE " + addIfNotExistsIfSupported() + + "${"t".inProperCase()} (" + + "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerAutoincType()} PRIMARY KEY, " + + "${"t1".inProperCase()} $timestampWithTimeZoneType${testTable.t1.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " + + "${"t2".inProperCase()} $timestampWithTimeZoneType${testTable.t2.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}" + + ")" + + 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 + ) + } else { + arrayListOf(baseExpression) + } + + assertEqualLists(expected, testTable.ddl) + + val id1 = testTable.insertAndGetId { } + + val row1 = testTable.select { testTable.id eq id1 }.single() + assertEqualDateTime(nowWithTimeZone, row1[testTable.t1]) + assertEqualDateTime(nowWithTimeZone, row1[testTable.t2]) + } + } + } } diff --git a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt index 3c58bb9480..aabb462d34 100644 --- a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt +++ b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/JavaTimeTests.kt @@ -1,15 +1,26 @@ package org.jetbrains.exposed +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.like import org.jetbrains.exposed.sql.javatime.* +import org.jetbrains.exposed.sql.json.extract +import org.jetbrains.exposed.sql.json.jsonb 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.tests.shared.assertTrue +import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.* import org.junit.Assert.fail import org.junit.Test @@ -19,6 +30,8 @@ import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneId import java.time.ZoneOffset import java.time.temporal.Temporal import kotlin.test.assertEquals @@ -54,21 +67,21 @@ open class JavaTimeBaseTest : DatabaseTestsBase() { // Checks that old numeric datetime columns works fine with new text representation @Test fun testSQLiteDateTimeFieldRegression() { - val TestDate = object : IntIdTable("TestDate") { + val testDate = object : IntIdTable("TestDate") { val time = datetime("time").defaultExpression(CurrentDateTime) } withDb(TestDB.SQLITE) { try { exec("CREATE TABLE IF NOT EXISTS TestDate (id INTEGER PRIMARY KEY AUTOINCREMENT, \"time\" NUMERIC DEFAULT (CURRENT_TIMESTAMP) NOT NULL);") - TestDate.insert { } - val year = TestDate.time.year() - val month = TestDate.time.month() - val day = TestDate.time.day() - val hour = TestDate.time.hour() - val minute = TestDate.time.minute() + testDate.insert { } + val year = testDate.time.year() + val month = testDate.time.month() + val day = testDate.time.day() + val hour = testDate.time.hour() + val minute = testDate.time.minute() - val result = TestDate.slice(year, month, day, hour, minute).selectAll().single() + val result = testDate.slice(year, month, day, hour, minute).selectAll().single() val now = LocalDateTime.now() assertEquals(now.year, result[year]) @@ -77,56 +90,65 @@ open class JavaTimeBaseTest : DatabaseTestsBase() { assertEquals(now.hour, result[hour]) assertEquals(now.minute, result[minute]) } finally { - SchemaUtils.drop(TestDate) + SchemaUtils.drop(testDate) } } } @Test - fun `test storing LocalDateTime with nanos`() { - val TestDate = object : IntIdTable("TestLocalDateTime") { + fun testStoringLocalDateTimeWithNanos() { + val testDate = object : IntIdTable("TestLocalDateTime") { val time = datetime("time") } - withTables(TestDate) { - val dateTimeWithNanos = LocalDateTime.now().withNano(123) - TestDate.insert { - it[time] = dateTimeWithNanos + + withTables(testDate) { + val dateTime = LocalDateTime.now() + val nanos = 111111 + // insert 2 separate nanosecond constants to ensure test's rounding mode matches DB precision + val dateTimeWithFewNanos = dateTime.withNano(nanos) + val dateTimeWithManyNanos = dateTime.withNano(nanos * 7) + testDate.insert { + it[time] = dateTimeWithFewNanos + } + testDate.insert { + it[time] = dateTimeWithManyNanos } - val dateTimeFromDB = TestDate.selectAll().single()[TestDate.time] - assertEqualDateTime(dateTimeWithNanos, dateTimeFromDB) + val dateTimesFromDB = testDate.selectAll().map { it[testDate.time] } + assertEqualDateTime(dateTimeWithFewNanos, dateTimesFromDB[0]) + assertEqualDateTime(dateTimeWithManyNanos, dateTimesFromDB[1]) } } @Test fun `test selecting Instant using expressions`() { - val TestTable = object : Table() { + val testTable = object : Table() { val ts = timestamp("ts") val tsn = timestamp("tsn").nullable() } val now = Instant.now() - withTables(TestTable) { - TestTable.insert { + withTables(testTable) { + testTable.insert { it[ts] = now it[tsn] = now } - val maxTsExpr = TestTable.ts.max() - val maxTimestamp = TestTable.slice(maxTsExpr).selectAll().single()[maxTsExpr] + val maxTsExpr = testTable.ts.max() + val maxTimestamp = testTable.slice(maxTsExpr).selectAll().single()[maxTsExpr] assertEqualDateTime(now, maxTimestamp) - val minTsExpr = TestTable.ts.min() - val minTimestamp = TestTable.slice(minTsExpr).selectAll().single()[minTsExpr] + val minTsExpr = testTable.ts.min() + val minTimestamp = testTable.slice(minTsExpr).selectAll().single()[minTsExpr] assertEqualDateTime(now, minTimestamp) - val maxTsnExpr = TestTable.tsn.max() - val maxNullableTimestamp = TestTable.slice(maxTsnExpr).selectAll().single()[maxTsnExpr] + val maxTsnExpr = testTable.tsn.max() + val maxNullableTimestamp = testTable.slice(maxTsnExpr).selectAll().single()[maxTsnExpr] assertEqualDateTime(now, maxNullableTimestamp) - val minTsnExpr = TestTable.tsn.min() - val minNullableTimestamp = TestTable.slice(minTsnExpr).selectAll().single()[minTsnExpr] + val minTsnExpr = testTable.tsn.min() + val minNullableTimestamp = testTable.slice(minTsnExpr).selectAll().single()[minTsnExpr] assertEqualDateTime(now, minNullableTimestamp) } } @@ -261,6 +283,176 @@ open class JavaTimeBaseTest : DatabaseTestsBase() { assertEquals(2, createdIn2023.size) } } + + @Test + fun testLocalDateTimeComparison() { + val testTableDT = object : IntIdTable("test_table_dt") { + val created = datetime("created") + val modified = datetime("modified") + } + + withTables(testTableDT) { testDb -> + val mayTheFourthDT = LocalDateTime.of(2011, 5, 4, 13, 0, 21, 871130789) + val nowDT = LocalDateTime.now() + val id1 = testTableDT.insertAndGetId { + it[created] = mayTheFourthDT + it[modified] = mayTheFourthDT + } + val id2 = testTableDT.insertAndGetId { + it[created] = mayTheFourthDT + it[modified] = nowDT + } + + // these DB take the nanosecond value 871_130_789 and round up to default precision (e.g. in Oracle: 871_131) + val requiresExplicitDTCast = listOf(TestDB.ORACLE, TestDB.H2_ORACLE, TestDB.H2_PSQL, TestDB.H2_SQLSERVER) + val dateTime = when (testDb) { + in requiresExplicitDTCast -> Cast(dateTimeParam(mayTheFourthDT), JavaLocalDateTimeColumnType()) + else -> dateTimeParam(mayTheFourthDT) + } + val createdMayFourth = testTableDT.select { testTableDT.created eq dateTime }.count() + assertEquals(2, createdMayFourth) + + val modifiedAtSameDT = testTableDT.select { testTableDT.modified eq testTableDT.created }.single() + assertEquals(id1, modifiedAtSameDT[testTableDT.id]) + + val modifiedAtLaterDT = testTableDT.select { testTableDT.modified greater testTableDT.created }.single() + assertEquals(id2, modifiedAtLaterDT[testTableDT.id]) + } + } + + @Test + fun testDateTimeAsJsonB() { + val tester = object : Table("tester") { + val created = datetime("created") + val modified = jsonb("modified", Json.Default) + } + + withTables(excludeSettings = TestDB.allH2TestDB + TestDB.SQLITE + TestDB.SQLSERVER + TestDB.ORACLE, tester) { + val dateTimeNow = LocalDateTime.now() + tester.insert { + it[created] = dateTimeNow.minusYears(1) + it[modified] = ModifierData(1, dateTimeNow) + } + tester.insert { + it[created] = dateTimeNow.plusYears(1) + it[modified] = ModifierData(2, dateTimeNow) + } + + val prefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + + // value extracted in same manner it is stored, a json string + val modifiedAsString = tester.modified.extract("${prefix}timestamp") + val allModifiedAsString = tester.slice(modifiedAsString).selectAll() + assertTrue(allModifiedAsString.all { it[modifiedAsString] == dateTimeNow.toString() }) + + // PostgreSQL requires explicit type cast to timestamp for in-DB comparison + val dateModified = if (currentDialectTest is PostgreSQLDialect) { + tester.modified.extract("${prefix}timestamp").castTo(JavaLocalDateTimeColumnType()) + } else { + tester.modified.extract("${prefix}timestamp") + } + val modifiedBeforeCreation = tester.select { dateModified less tester.created }.single() + assertEquals(2, modifiedBeforeCreation[tester.modified].userId) + } + } + + @Test + fun testTimestampWithTimeZone() { + val testTable = object : IntIdTable("TestTable") { + val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column") + } + + withDb(excludeSettings = listOf(TestDB.MARIADB)) { testDB -> + if (!isOldMySql()) { + SchemaUtils.create(testTable) + + // Cairo time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Africa/Cairo")) + assertEquals("Africa/Cairo", ZoneId.systemDefault().id) + + val cairoNow = OffsetDateTime.now(ZoneId.systemDefault()) + + val cairoId = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInCairoTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + // UTC time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC)) + assertEquals("UTC", ZoneId.systemDefault().id) + + val cairoNowRetrievedInUTCTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + val utcID = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInUTCTimeZone = testTable.select { testTable.id eq utcID } + .single()[testTable.timestampWithTimeZone] + + // Tokyo time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Asia/Tokyo")) + assertEquals("Asia/Tokyo", ZoneId.systemDefault().id) + + val cairoNowRetrievedInTokyoTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + val tokyoID = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInTokyoTimeZone = testTable.select { testTable.id eq tokyoID } + .single()[testTable.timestampWithTimeZone] + + // PostgreSQL and MySQL always store the timestamp in UTC, thereby losing the original time zone. + // To preserve the original time zone, store the time zone information in a separate column. + val isOriginalTimeZonePreserved = testDB !in listOf( + TestDB.POSTGRESQL, + TestDB.POSTGRESQLNG, + TestDB.MYSQL + ) + if (isOriginalTimeZonePreserved) { + // Assert that time zone is preserved when the same value is inserted in different time zones + assertEqualDateTime(cairoNow, cairoNowInsertedInCairoTimeZone) + assertEqualDateTime(cairoNow, cairoNowInsertedInUTCTimeZone) + assertEqualDateTime(cairoNow, cairoNowInsertedInTokyoTimeZone) + + // Assert that time zone is preserved when the same record is retrieved in different time zones + assertEqualDateTime(cairoNow, cairoNowRetrievedInUTCTimeZone) + assertEqualDateTime(cairoNow, cairoNowRetrievedInTokyoTimeZone) + } else { + // Assert equivalence in UTC when the same value is inserted in different time zones + assertEqualDateTime(cairoNowInsertedInCairoTimeZone, cairoNowInsertedInUTCTimeZone) + assertEqualDateTime(cairoNowInsertedInUTCTimeZone, cairoNowInsertedInTokyoTimeZone) + + // Assert equivalence in UTC when the same record is retrieved in different time zones + assertEqualDateTime(cairoNowRetrievedInUTCTimeZone, cairoNowRetrievedInTokyoTimeZone) + } + + // Reset to original time zone as set up in DatabaseTestsBase init block + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC)) + assertEquals("UTC", ZoneId.systemDefault().id) + } + } + } + + @Test + fun testTimestampWithTimeZoneThrowsExceptionForUnsupportedDialects() { + val testTable = object : IntIdTable("TestTable") { + val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column") + } + + withDb(db = listOf(TestDB.MYSQL, TestDB.MARIADB)) { testDB -> + if (testDB == TestDB.MARIADB || isOldMySql()) { + expectException { + SchemaUtils.create(testTable) + } + } + } + } } fun assertEqualDateTime(d1: T?, d2: T?) { @@ -275,36 +467,49 @@ fun assertEqualDateTime(d1: T?, d2: T?) { } } d1 is LocalDateTime && d2 is LocalDateTime -> { - assertEquals(d1.toEpochSecond(ZoneOffset.UTC), d2.toEpochSecond(ZoneOffset.UTC), "Failed on epoch seconds ${currentDialectTest.name}") + assertEquals( + d1.toEpochSecond(ZoneOffset.UTC), + d2.toEpochSecond(ZoneOffset.UTC), + "Failed on epoch seconds ${currentDialectTest.name}" + ) assertEqualFractionalPart(d1.nano, d2.nano) } d1 is Instant && d2 is Instant -> { assertEquals(d1.epochSecond, d2.epochSecond, "Failed on epoch seconds ${currentDialectTest.name}") assertEqualFractionalPart(d1.nano, d2.nano) } + d1 is OffsetDateTime && d2 is OffsetDateTime -> { + assertEqualDateTime(d1.toLocalDateTime(), d2.toLocalDateTime()) + assertEquals(d1.offset, d2.offset) + } else -> assertEquals(d1, d2, "Failed on ${currentDialectTest.name}") } } private fun assertEqualFractionalPart(nano1: Int, nano2: Int) { - when (currentDialectTest) { - // nanoseconds (H2, Oracle & Sqlite could be here) - // assertEquals(nano1, nano2, "Failed on nano ${currentDialectTest.name}") + val dialect = currentDialectTest + val db = dialect.name + when (dialect) { // accurate to 100 nanoseconds - is SQLServerDialect -> assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds ${currentDialectTest.name}") + is SQLServerDialect -> + assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds $db") // microseconds - is H2Dialect, is MariaDBDialect, is PostgreSQLDialect, is PostgreSQLNGDialect -> assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") - is MysqlDialect -> - if ((currentDialectTest as? MysqlDialect)?.isFractionDateTimeSupported() == true) { - // this should be uncommented, but mysql has different microseconds between save & read -// assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") - } else { - // don't compare fractional part + is MariaDBDialect -> + assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db") + is H2Dialect, is PostgreSQLDialect, is MysqlDialect -> { + when ((dialect as? MysqlDialect)?.isFractionDateTimeSupported()) { + null, true -> { + assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") + } + else -> {} // don't compare fractional part } + } // milliseconds - is OracleDialect -> assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - is SQLiteDialect -> assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - else -> fail("Unknown dialect ${currentDialectTest.name}") + is OracleDialect -> + assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds $db") + is SQLiteDialect -> + assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds $db") + else -> fail("Unknown dialect $db") } } @@ -316,6 +521,8 @@ private fun roundToMicro(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000), RoundingMode.HALF_UP).toInt() } +private fun floorToMicro(nanos: Int): Int = nanos / 1_000 + private fun roundToMilli(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000_000), RoundingMode.HALF_UP).toInt() } @@ -337,3 +544,16 @@ object CitiesTime : IntIdTable("CitiesTime") { val name = varchar("name", 50) // Column val local_time = datetime("local_time").nullable() // Column } + +@Serializable +data class ModifierData( + val userId: Int, + @Serializable(with = DateTimeSerializer::class) + val timestamp: LocalDateTime +) + +object DateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: LocalDateTime) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): LocalDateTime = LocalDateTime.parse(decoder.decodeString()) +} diff --git a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/MiscTableTest.kt b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/MiscTableTest.kt index 2102982d70..7faa270527 100644 --- a/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/MiscTableTest.kt +++ b/exposed-java-time/src/test/kotlin/org/jetbrains/exposed/MiscTableTest.kt @@ -1,11 +1,9 @@ +@file:Suppress("MaximumLineLength", "LongMethod") + package org.jetbrains.exposed import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.javatime.date -import org.jetbrains.exposed.sql.javatime.datetime -import org.jetbrains.exposed.sql.javatime.duration -import org.jetbrains.exposed.sql.javatime.time -import org.jetbrains.exposed.sql.javatime.timestamp +import org.jetbrains.exposed.sql.javatime.* import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.shared.MiscTable @@ -250,10 +248,14 @@ class MiscTableTest : DatabaseTestsBase() { } } + // these DB take the datetime nanosecond value and round up to default precision + // which causes flaky comparison failures if not cast to TIMESTAMP first + private val requiresExplicitDTCast = listOf(TestDB.ORACLE, TestDB.H2_ORACLE, TestDB.H2_PSQL, TestDB.H2_SQLSERVER) + @Test fun testSelect01() { val tbl = Misc - withTables(tbl) { + withTables(tbl) { testDb -> val date = today val time = LocalTime.now() val dateTime = LocalDateTime.now() @@ -434,8 +436,12 @@ class MiscTableTest : DatabaseTestsBase() { dblcn = null ) + val dtValue = when (testDb) { + in requiresExplicitDTCast -> Cast(dateTimeParam(dateTime), JavaLocalDateTimeColumnType()) + else -> dateTimeParam(dateTime) + } tbl.checkRowFull( - tbl.select { tbl.dt.eq(dateTime) }.single(), + tbl.select { tbl.dt.eq(dtValue) }.single(), by = 13, byn = null, sm = -10, @@ -690,7 +696,7 @@ class MiscTableTest : DatabaseTestsBase() { @Test fun testSelect02() { val tbl = Misc - withTables(tbl) { + withTables(tbl) { testDb -> val date = today val time = LocalTime.now() val dateTime = LocalDateTime.now() @@ -856,8 +862,12 @@ class MiscTableTest : DatabaseTestsBase() { dblcn = 567.89 ) + val dtValue = when (testDb) { + in requiresExplicitDTCast -> Cast(dateTimeParam(dateTime), JavaLocalDateTimeColumnType()) + else -> dateTimeParam(dateTime) + } tbl.checkRowFull( - tbl.select { tbl.dt.eq(dateTime) }.single(), + tbl.select { tbl.dt.eq(dtValue) }.single(), by = 13, byn = 13, sm = -10, @@ -1240,7 +1250,7 @@ class MiscTableTest : DatabaseTestsBase() { exec("INSERT IGNORE INTO `zerodatetimetable` (dt1,dt2,ts1,ts2) VALUES ('0000-00-00 00:00:00', '0000-00-00 00:00:00', '0000-00-00 00:00:00', '0000-00-00 00:00:00');") val row = ZeroDateTimeTable.selectAll().first() - for (c in listOf(ZeroDateTimeTable.dt1, ZeroDateTimeTable.dt2, ZeroDateTimeTable.ts1, ZeroDateTimeTable.ts2)) { + listOf(ZeroDateTimeTable.dt1, ZeroDateTimeTable.dt2, ZeroDateTimeTable.ts1, ZeroDateTimeTable.ts2).forEach { c -> val actual = row[c] assertNull(actual, "$c expected null but was $actual") } 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/api/exposed-jdbc.api b/exposed-jdbc/api/exposed-jdbc.api new file mode 100644 index 0000000000..d2401db896 --- /dev/null +++ b/exposed-jdbc/api/exposed-jdbc.api @@ -0,0 +1,83 @@ +public final class org/jetbrains/exposed/jdbc/ExposedConnectionImpl : org/jetbrains/exposed/sql/DatabaseConnectionAutoRegistration { + public fun ()V + public synthetic fun invoke (Ljava/lang/Object;)Ljava/lang/Object; + public fun invoke (Ljava/sql/Connection;)Lorg/jetbrains/exposed/sql/statements/jdbc/JdbcConnectionImpl; +} + +public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcConnectionImpl : org/jetbrains/exposed/sql/statements/api/ExposedConnection { + public fun (Ljava/sql/Connection;)V + public fun close ()V + public fun commit ()V + public fun executeInBatch (Ljava/util/List;)V + public fun getAutoCommit ()Z + public fun getCatalog ()Ljava/lang/String; + public synthetic fun getConnection ()Ljava/lang/Object; + public fun getConnection ()Ljava/sql/Connection; + public fun getReadOnly ()Z + public fun getSchema ()Ljava/lang/String; + public fun getTransactionIsolation ()I + public fun isClosed ()Z + public fun metadata (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public fun prepareStatement (Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + public fun prepareStatement (Ljava/lang/String;[Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; + public fun releaseSavepoint (Lorg/jetbrains/exposed/sql/statements/api/ExposedSavepoint;)V + public fun rollback ()V + public fun rollback (Lorg/jetbrains/exposed/sql/statements/api/ExposedSavepoint;)V + public fun setAutoCommit (Z)V + public fun setCatalog (Ljava/lang/String;)V + public fun setReadOnly (Z)V + public fun setSavepoint (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/ExposedSavepoint; + public fun setSchema (Ljava/lang/String;)V + public fun setTransactionIsolation (I)V +} + +public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl : org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata { + public static final field Companion Lorg/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl$Companion; + public fun (Ljava/lang/String;Ljava/sql/DatabaseMetaData;)V + public fun cleanCache ()V + public fun columns ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public fun existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public fun existingPrimaryKeys ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; + public fun getCurrentScheme ()Ljava/lang/String; + public fun getDatabaseDialectName ()Ljava/lang/String; + public fun getDatabaseProductVersion ()Ljava/lang/String; + public fun getDefaultIsolationLevel ()I + public fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi; + public final fun getMetadata ()Ljava/sql/DatabaseMetaData; + public fun getSchemaNames ()Ljava/util/List; + public fun getSupportsAlterTableWithAddColumn ()Z + public fun getSupportsMultipleResultSets ()Z + public fun getSupportsSelectForUpdate ()Z + public fun getTableNames ()Ljava/util/Map; + public fun getUrl ()Ljava/lang/String; + public fun getVersion ()Ljava/math/BigDecimal; + public fun resetCurrentScheme ()V + public fun tableConstraints (Ljava/util/List;)Ljava/util/Map; +} + +public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl$Companion { +} + +public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcPreparedStatementImpl : org/jetbrains/exposed/sql/statements/api/PreparedStatementApi { + public fun (Ljava/sql/PreparedStatement;ZZ)V + public fun addBatch ()V + public fun cancel ()V + public fun closeIfPossible ()V + public fun executeBatch ()Ljava/util/List; + public fun executeQuery ()Ljava/sql/ResultSet; + public fun executeUpdate ()I + public fun fillParameters (Ljava/lang/Iterable;)I + public fun getFetchSize ()Ljava/lang/Integer; + public fun getResultSet ()Ljava/sql/ResultSet; + public final fun getStatement ()Ljava/sql/PreparedStatement; + public final fun getWasGeneratedKeysRequested ()Z + public fun set (ILjava/lang/Object;)V + public fun setFetchSize (Ljava/lang/Integer;)V + public fun setInputStream (ILjava/io/InputStream;)V + public fun setNull (ILorg/jetbrains/exposed/sql/IColumnType;)V +} + +public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcSavepoint : org/jetbrains/exposed/sql/statements/api/ExposedSavepoint { + public fun (Ljava/lang/String;Ljava/sql/Savepoint;)V +} + diff --git a/exposed-jdbc/build.gradle.kts b/exposed-jdbc/build.gradle.kts index bf2454a9cc..9221cccc4e 100644 --- a/exposed-jdbc/build.gradle.kts +++ b/exposed-jdbc/build.gradle.kts @@ -6,6 +6,10 @@ repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) } diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcConnectionImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcConnectionImpl.kt index 6a62e9a46b..4ae31e1116 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcConnectionImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcConnectionImpl.kt @@ -74,11 +74,19 @@ class JdbcConnectionImpl(override val connection: Connection) : ExposedConnectio } else { PreparedStatement.NO_GENERATED_KEYS } - return JdbcPreparedStatementImpl(connection.prepareStatement(sql, generated), returnKeys) + return JdbcPreparedStatementImpl( + connection.prepareStatement(sql, generated), + returnKeys, + connection.metaData.supportsGetGeneratedKeys() + ) } override fun prepareStatement(sql: String, columns: Array): PreparedStatementApi { - return JdbcPreparedStatementImpl(connection.prepareStatement(sql, columns), true) + return JdbcPreparedStatementImpl( + connection.prepareStatement(sql, columns), + true, + connection.metaData.supportsGetGeneratedKeys() + ) } override fun executeInBatch(sqls: List) { @@ -119,7 +127,7 @@ class JdbcConnectionImpl(override val connection: Connection) : ExposedConnectio executeUpdate() } - override fun prepareSQL(transaction: Transaction): String = sqls.joinToString("\n") + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = sqls.joinToString("\n") override fun arguments(): Iterable>> = emptyList() } 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 fa5371d800..fc94340607 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 @@ -1,11 +1,6 @@ package org.jetbrains.exposed.sql.statements.jdbc -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ForeignKeyConstraint -import org.jetbrains.exposed.sql.Index -import org.jetbrains.exposed.sql.ReferenceOption -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.ExposedDatabaseMetadata import org.jetbrains.exposed.sql.statements.api.IdentifierManagerApi import org.jetbrains.exposed.sql.transactions.TransactionManager @@ -22,9 +17,8 @@ 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 "H2 JDBC Driver" -> H2Dialect.dialectName @@ -68,7 +62,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) field = try { when (databaseDialectName) { MysqlDialect.dialectName, MariaDBDialect.dialectName -> metadata.connection.catalog.orEmpty() - OracleDialect.dialectName -> databaseName + OracleDialect.dialectName -> metadata.connection.schema ?: databaseName else -> metadata.connection.schema.orEmpty() } } catch (_: Throwable) { @@ -84,18 +78,19 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) _currentScheme = null } - private inner class CachableMapWithDefault(private val map: MutableMap = mutableMapOf(), val default: (K) -> V) : Map by map { + private inner class CachableMapWithDefault( + private val map: MutableMap = mutableMapOf(), + val default: (K) -> V + ) : Map by map { override fun get(key: K): V? = map.getOrPut(key) { default(key) } override fun containsKey(key: K): Boolean = true override fun isEmpty(): Boolean = false } 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 @@ -130,70 +125,82 @@ 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.nameInDatabaseCase() } - 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 } override fun columns(vararg tables: Table): Map> { - val rs = metadata.getColumns(databaseName, currentScheme, "%", "%") - val result = rs.extractColumns(tables) { - // @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 + val result = mutableMapOf>() + val useSchemaInsteadOfDatabase = currentDialect is MysqlDialect + val tablesBySchema = tables.groupBy { identifierManager.inProperCase(it.schemaName ?: currentScheme) } + + 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('\'') else -> defaultValue } + else -> defaultValue.trim('\'') } } private val existingIndicesCache = HashMap>() + @Suppress("CyclomaticComplexMethod") override fun existingIndices(vararg tables: Table): Map> { for (table in tables) { - val tableName = table.nameInDatabaseCase() val transaction = TransactionManager.current() + val (catalog, tableSchema) = tableCatalogAndSchema(table) existingIndicesCache.getOrPut(table) { - val pkNames = metadata.getPrimaryKeys(databaseName, currentScheme, tableName).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 } @@ -201,36 +208,74 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) rs.close() names } - val rs = metadata.getIndexInfo(databaseName, currentScheme, tableName, false, false) + val storedIndexTable = if (tableSchema == currentScheme) table.nameInDatabaseCase() else table.nameInDatabaseCaseUnquoted() + val rs = metadata.getIndexInfo(catalog, tableSchema, storedIndexTable, false, false) - val tmpIndices = hashMapOf, MutableList>() + val tmpIndices = hashMapOf, MutableList>() while (rs.next()) { - rs.getString("INDEX_NAME")?.let { - val column = transaction.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(rs.getString("COLUMN_NAME")!!) - val isUnique = !rs.getBoolean("NON_UNIQUE") - tmpIndices.getOrPut(it to isUnique) { arrayListOf() }.add(column) + rs.getString("INDEX_NAME")?.let { indexName -> + // if index is function-based, SQLite & MySQL return null column_name metadata + val columnNameMetadata = rs.getString("COLUMN_NAME") ?: when (currentDialect) { + is MysqlDialect, is SQLiteDialect -> "\"\"" + else -> null + } + columnNameMetadata?.let { 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) + } } } rs.close() val tColumns = table.columns.associateBy { transaction.identity(it) } - tmpIndices.filterNot { it.key.first in pkNames } - .mapNotNull { (index, columns) -> - columns.distinct().mapNotNull { cn -> tColumns[cn] }.takeIf { c -> c.size == columns.size } - ?.let { c -> Index(c, index.second, index.first) } + 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) } + override fun existingPrimaryKeys(vararg tables: Table): Map { + return tables.associateWith { table -> + val (catalog, tableSchema) = tableCatalogAndSchema(table) + metadata.getPrimaryKeys(catalog, tableSchema, table.nameInDatabaseCaseUnquoted()).let { rs -> + val columnNames = mutableListOf() + var pkName = "" + while (rs.next()) { + rs.getString("PK_NAME")?.let { pkName = it } + columnNames += rs.getString("COLUMN_NAME") + } + rs.close() + if (pkName.isEmpty()) null else PrimaryKeyMetadata(pkName, columnNames) + } + } + } + @Synchronized override fun tableConstraints(tables: List
): Map> { - val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCase() } + val allTables = SchemaUtils.sortTablesByReferences(tables).associateBy { it.nameInDatabaseCaseUnquoted() } return allTables.keys.associateWith { table -> - metadata.getImportedKeys(databaseName, currentScheme, table).iterate { + 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 @@ -242,8 +287,10 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) val targetColumn = allTables[targetTableName]?.columns?.firstOrNull { identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(it.nameInDatabaseCase()) == targetColumnName } ?: return@iterate null // Do not crash if there are missing fields in Exposed's tables - val constraintUpdateRule = ReferenceOption.resolveRefOptionFromJdbc(getInt("UPDATE_RULE")) - val constraintDeleteRule = ReferenceOption.resolveRefOptionFromJdbc(getInt("DELETE_RULE")) + val constraintUpdateRule = getObject("UPDATE_RULE")?.toString()?.toIntOrNull()?.let { + currentDialect.resolveRefOptionFromJdbc(it) + } + val constraintDeleteRule = currentDialect.resolveRefOptionFromJdbc(getInt("DELETE_RULE")) ForeignKeyConstraint( target = targetColumn, from = fromColumn, @@ -251,11 +298,25 @@ 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) } + } + } + + /** + * Returns the name of the database in which a [table] is found, as well as it's schema name. + * + * If the table name does not include a schema prefix, the metadata value `currentScheme` is used instead. + * + * MySQL/MariaDB are special cases in that a schema definition is treated like a separate database. This means that + * a connection to 'testDb' with a table defined as 'my_schema.my_table' will only successfully find the table's + * metadata if 'my_schema' is used as the database name. + */ + private fun tableCatalogAndSchema(table: Table): Pair { + val tableSchema = identifierManager.inProperCase(table.schemaName ?: currentScheme) + return if (currentDialect is MysqlDialect && tableSchema != currentScheme) { + tableSchema to tableSchema + } else { + databaseName to tableSchema } } diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcIdentifierManager.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcIdentifierManager.kt index 332ff5bfe2..5f0f87001b 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcIdentifierManager.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcIdentifierManager.kt @@ -14,6 +14,8 @@ internal class JdbcIdentifierManager(metadata: DatabaseMetaData) : IdentifierMan private val _keywords = metadata.sqlKeywords.split(',') override fun dbKeywords(): List = _keywords override val extraNameCharacters = metadata.extraNameCharacters!! + + @Suppress("MagicNumber") override val oracleVersion = when { metadata.databaseProductName != "Oracle" -> OracleVersion.NonOracle metadata.databaseMajorVersion <= 11 -> OracleVersion.Oracle11g diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcPreparedStatementImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcPreparedStatementImpl.kt index e6edcac443..7b74838064 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcPreparedStatementImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcPreparedStatementImpl.kt @@ -4,18 +4,34 @@ import org.jetbrains.exposed.sql.BinaryColumnType import org.jetbrains.exposed.sql.BlobColumnType import org.jetbrains.exposed.sql.IColumnType import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi +import org.jetbrains.exposed.sql.vendors.SQLiteDialect +import org.jetbrains.exposed.sql.vendors.currentDialect import java.io.InputStream import java.sql.PreparedStatement import java.sql.ResultSet +import java.sql.Statement import java.sql.Types -class JdbcPreparedStatementImpl(val statement: PreparedStatement, val wasGeneratedKeysRequested: Boolean) : PreparedStatementApi { +class JdbcPreparedStatementImpl( + val statement: PreparedStatement, + val wasGeneratedKeysRequested: Boolean, + private val supportsGetGeneratedKeys: Boolean +) : PreparedStatementApi { override val resultSet: ResultSet? - get() = if (wasGeneratedKeysRequested) statement.generatedKeys else statement.resultSet + get() = when { + !wasGeneratedKeysRequested -> statement.resultSet + supportsGetGeneratedKeys -> statement.generatedKeys + currentDialect is SQLiteDialect -> { + statement.connection.prepareStatement("select last_insert_rowid();").executeQuery() + } + else -> statement.resultSet + } override var fetchSize: Int? get() = statement.fetchSize - set(value) { value?.let { statement.fetchSize = value } } + set(value) { + value?.let { statement.fetchSize = value } + } override fun addBatch() { statement.addBatch() @@ -45,7 +61,15 @@ class JdbcPreparedStatementImpl(val statement: PreparedStatement, val wasGenerat if (!statement.isClosed) statement.close() } - override fun executeBatch(): List = statement.executeBatch().toList() + override fun executeBatch(): List { + return statement.executeBatch().map { + when (it) { + Statement.SUCCESS_NO_INFO -> 1 + Statement.EXECUTE_FAILED -> 0 + else -> it + } + } + } override fun cancel() { if (!statement.isClosed) statement.cancel() diff --git a/exposed-jodatime/api/exposed-jodatime.api b/exposed-jodatime/api/exposed-jodatime.api new file mode 100644 index 0000000000..9d185761be --- /dev/null +++ b/exposed-jodatime/api/exposed-jodatime.api @@ -0,0 +1,101 @@ +public final class org/jetbrains/exposed/sql/jodatime/CurrentDate : org/jetbrains/exposed/sql/Function { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/jodatime/CurrentDate; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/CurrentDateTime : org/jetbrains/exposed/sql/Function { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/jodatime/CurrentDateTime; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/Date : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/DateColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public fun (Z)V + public fun equals (Ljava/lang/Object;)Z + public fun getHasTimePart ()Z + public final fun getTime ()Z + public fun hashCode ()I + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/jodatime/DateColumnTypeKt { + public static final fun date (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun datetime (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun timestampWithTimeZone (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/jodatime/DateFunctionsKt { + public static final fun CustomDateFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomDateTimeFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimestampWithTimeZoneFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun date (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Date; + public static final fun dateLiteral (Lorg/joda/time/DateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun dateParam (Lorg/joda/time/DateTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun dateTimeLiteral (Lorg/joda/time/DateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun dateTimeParam (Lorg/joda/time/DateTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun day (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Day; + public static final fun hour (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Hour; + public static final fun minute (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Minute; + public static final fun month (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Month; + public static final fun second (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Second; + public static final fun timestampWithTimeZoneLiteral (Lorg/joda/time/DateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timestampWithTimeZoneParam (Lorg/joda/time/DateTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun year (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/jodatime/Year; +} + +public final class org/jetbrains/exposed/sql/jodatime/DateTimeWithTimeZoneColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Lorg/joda/time/DateTime; +} + +public final class org/jetbrains/exposed/sql/jodatime/Day : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/Hour : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/Minute : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/Month : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/Second : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/jodatime/Year : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + diff --git a/exposed-jodatime/build.gradle.kts b/exposed-jodatime/build.gradle.kts index 5812fde905..564fef99c1 100644 --- a/exposed-jodatime/build.gradle.kts +++ b/exposed-jodatime/build.gradle.kts @@ -1,19 +1,22 @@ -import org.jetbrains.exposed.gradle.Versions - plugins { kotlin("jvm") apply true - id("testWithDBs") + kotlin("plugin.serialization") apply true } repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) api("joda-time", "joda-time", "2.10.13") testImplementation(project(":exposed-dao")) testImplementation(project(":exposed-tests")) + testImplementation(project(":exposed-json")) testImplementation("junit", "junit", "4.12") testImplementation(kotlin("test-junit")) } diff --git a/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateColumnType.kt b/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateColumnType.kt index 73a65a36cf..ebd367a869 100644 --- a/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateColumnType.kt +++ b/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateColumnType.kt @@ -4,8 +4,8 @@ import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.ColumnType import org.jetbrains.exposed.sql.IDateColumnType import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.vendors.MariaDBDialect import org.jetbrains.exposed.sql.vendors.MysqlDialect +import org.jetbrains.exposed.sql.vendors.OracleDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect import org.jetbrains.exposed.sql.vendors.currentDialect import org.joda.time.DateTime @@ -21,7 +21,25 @@ private val DEFAULT_DATE_TIME_STRING_FORMATTER = DateTimeFormat.forPattern("YYYY private val SQLITE_AND_ORACLE_DATE_TIME_STRING_FORMATTER = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss.SSS") private val SQLITE_DATE_STRING_FORMATTER = ISODateTimeFormat.yearMonthDay() -private fun formatterForDateTimeString(date: String) = dateTimeWithFractionFormat(date.substringAfterLast('.', "").length) +private val SQLITE_DATE_TIME_WITH_TIME_ZONE_FORMATTER by lazy { + DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSZZ").withLocale(Locale.ROOT) +} + +private val MYSQL_DATE_TIME_WITH_TIME_ZONE_FORMATTER by lazy { + DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSSZZ").withLocale(Locale.ROOT) +} + +private val ORACLE_DATE_TIME_WITH_TIME_ZONE_FORMATTER by lazy { + DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSS ZZ").withLocale(Locale.ROOT) +} + +private val DEFAULT_DATE_TIME_WITH_TIME_ZONE_FORMATTER by lazy { + ISODateTimeFormat.dateTime().withLocale(Locale.ROOT) +} + +private fun formatterForDateTimeString(date: String) = dateTimeWithFractionFormat( + date.substringAfterLast('.', "").length +) private fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter { val baseFormat = "YYYY-MM-dd HH:mm:ss" val newFormat = baseFormat + if (fraction in 1..9) ".${"S".repeat(fraction)}" else "" @@ -65,14 +83,8 @@ class DateColumnType(val time: Boolean) : ColumnType(), IDateColumnType { currentDialect is SQLiteDialect -> SQLITE_DATE_STRING_FORMATTER.parseDateTime(value) else -> DEFAULT_DATE_STRING_FORMATTER.parseDateTime(value) } - - else -> { - if (localDateTimeClass == value.javaClass) { - DateTime.parse(value.toString()) - } else { - valueFromDB(value.toString()) as DateTime - } - } + is java.time.LocalDateTime -> DateTime.parse(value.toString()) + else -> valueFromDB(value.toString()) as DateTime } return when (time) { true -> dateTime @@ -81,18 +93,12 @@ class DateColumnType(val time: Boolean) : ColumnType(), IDateColumnType { } override fun readObject(rs: ResultSet, index: Int): Any? { - /* - Since MySQL ConnectorJ 8.0.23 driver returns LocalDateTime instead of String for DateTime columns. - As exposed-jodatime module should work with Java 1.7 it's necessary to fetch it as String as it was before. - - MariaDB however may return '0000-00-00 00:00:00' on getString even though it is also null in many other - regards (and can obviously never be converted to anything reasonable). So dont do that for MariaDB. - */ - return if (time && localDateTimeClass != null && currentDialect is MysqlDialect) { - rs.getObject(index, localDateTimeClass) - } else if (time && currentDialect is MysqlDialect && currentDialect !is MariaDBDialect) { - rs.getObject(index, String::class.java) - } else super.readObject(rs, index) + // Since MySQL ConnectorJ 8.0.23, driver returns LocalDateTime instead of String for DateTime columns. + return if (time && currentDialect is MysqlDialect) { + rs.getObject(index, java.time.LocalDateTime::class.java) + } else { + super.readObject(rs, index) + } } override fun notNullValueToDB(value: Any): Any = when { @@ -118,14 +124,53 @@ class DateColumnType(val time: Boolean) : ColumnType(), IDateColumnType { result = 31 * result + time.hashCode() return result } +} + +class DateTimeWithTimeZoneColumnType : ColumnType(), IDateColumnType { + override val hasTimePart: Boolean = true + + override fun sqlType(): String = currentDialect.dataTypeProvider.timestampWithTimeZoneType() + + override fun nonNullValueToString(value: Any): String = when (value) { + is DateTime -> { + when (currentDialect) { + is SQLiteDialect -> "'${SQLITE_DATE_TIME_WITH_TIME_ZONE_FORMATTER.print(value)}'" + is MysqlDialect -> "'${MYSQL_DATE_TIME_WITH_TIME_ZONE_FORMATTER.print(value)}'" + is OracleDialect -> "'${ORACLE_DATE_TIME_WITH_TIME_ZONE_FORMATTER.print(value)}'" + else -> "'${DEFAULT_DATE_TIME_WITH_TIME_ZONE_FORMATTER.print(value)}'" + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + override fun valueFromDB(value: Any): DateTime = when (value) { + is java.time.OffsetDateTime -> DateTime.parse(value.toString()) + is String -> { + if (currentDialect is SQLiteDialect) { + DateTime.parse(value, SQLITE_DATE_TIME_WITH_TIME_ZONE_FORMATTER) + } else { + DateTime.parse(value) + } + } + is java.sql.Timestamp -> DateTime(value.time) + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + override fun readObject(rs: ResultSet, index: Int): Any? = when (currentDialect) { + is SQLiteDialect -> super.readObject(rs, index) + is OracleDialect -> rs.getObject(index, java.sql.Timestamp::class.java) + else -> rs.getObject(index, java.time.OffsetDateTime::class.java) + } - companion object { - // https://www.baeldung.com/java-check-class-exists - private val localDateTimeClass = try { - Class.forName("java.time.LocalDateTime", false, this::class.java.classLoader) - } catch (_: ClassNotFoundException) { - null + override fun notNullValueToDB(value: Any): Any = when (value) { + is DateTime -> { + when (currentDialect) { + is SQLiteDialect -> SQLITE_DATE_TIME_WITH_TIME_ZONE_FORMATTER.print(value) + is MysqlDialect -> MYSQL_DATE_TIME_WITH_TIME_ZONE_FORMATTER.print(value) + else -> java.sql.Timestamp(value.millis) + } } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") } } @@ -142,3 +187,13 @@ fun Table.date(name: String): Column = registerColumn(name, DateColumn * @param name The column name */ fun Table.datetime(name: String): Column = registerColumn(name, DateColumnType(true)) + +/** + * A timestamp column to store both a date and a time with time zone. + * + * Note: PostgreSQL and MySQL always store the timestamp in UTC, thereby losing the original time zone. To preserve the + * original time zone, store the time zone information in a separate column. + * + * @param name The column name + */ +fun Table.timestampWithTimeZone(name: String): Column = registerColumn(name, DateTimeWithTimeZoneColumnType()) diff --git a/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateFunctions.kt b/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateFunctions.kt index da9c554863..fef582edd0 100644 --- a/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateFunctions.kt +++ b/exposed-jodatime/src/main/kotlin/org/jetbrains/exposed/sql/jodatime/DateFunctions.kt @@ -20,14 +20,6 @@ object CurrentDateTime : Function(DateColumnType(true)) { else -> "CURRENT_TIMESTAMP" } } - - @Deprecated( - message = "This class is now a singleton, no need for its constructor call; " + - "this method is provided for backward-compatibility only, and will be removed in future releases", - replaceWith = ReplaceWith("this"), - level = DeprecationLevel.ERROR, - ) - operator fun invoke() = this } object CurrentDate : Function(DateColumnType(false)) { @@ -117,12 +109,22 @@ fun Expression.second() = Second(this) fun dateParam(value: DateTime): Expression = QueryParameter(value, DateColumnType(false)) fun dateTimeParam(value: DateTime): Expression = QueryParameter(value, DateColumnType(true)) +fun timestampWithTimeZoneParam(value: DateTime): Expression = + QueryParameter(value, DateTimeWithTimeZoneColumnType()) fun dateLiteral(value: DateTime): LiteralOp = LiteralOp(DateColumnType(false), value) fun dateTimeLiteral(value: DateTime): LiteralOp = LiteralOp(DateColumnType(true), value) +fun timestampWithTimeZoneLiteral(value: DateTime): LiteralOp = + LiteralOp(DateTimeWithTimeZoneColumnType(), value) +@Suppress("FunctionNaming") fun CustomDateTimeFunction(functionName: String, vararg params: Expression<*>) = CustomFunction(functionName, DateColumnType(true), *params) +@Suppress("FunctionNaming") fun CustomDateFunction(functionName: String, vararg params: Expression<*>) = CustomFunction(functionName, DateColumnType(false), *params) + +@Suppress("FunctionNaming") +fun CustomTimestampWithTimeZoneFunction(functionName: String, vararg params: Expression<*>) = + CustomFunction(functionName, DateTimeWithTimeZoneColumnType(), *params) 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 7e881e76c4..3853c94c19 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeDefaultsTest.kt @@ -10,6 +10,7 @@ import org.jetbrains.exposed.sql.jodatime.* import org.jetbrains.exposed.sql.statements.BatchDataInconsistentException import org.jetbrains.exposed.sql.statements.BatchInsertStatement import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.constraintNamePart import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.inProperCase import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections @@ -28,6 +29,7 @@ import org.joda.time.DateTimeZone import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertTrue class JodaTimeDefaultsTest : JodaTimeBaseTest() { object TableWithDBDefault : IntIdTable() { @@ -45,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() @@ -154,7 +158,7 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { } val dtConstValue = DateTime.parse("2010-01-01").withZone(DateTimeZone.UTC) val dtLiteral = dateLiteral(dtConstValue) - val TestTable = object : IntIdTable("t") { + val testTable = object : IntIdTable("t") { val s = varchar("s", 100).default("test") val sn = varchar("sn", 100).default("testNullable").nullable() val l = long("l").default(42) @@ -168,53 +172,56 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { fun Expression<*>.itOrNull() = when { currentDialectTest.isAllowedAsColumnDefault(this) -> "DEFAULT ${currentDialectTest.dataTypeProvider.processForDefaultValue(this)} NOT NULL" + else -> "NULL" } - withTables(listOf(TestDB.SQLITE), TestTable) { + withTables(listOf(TestDB.SQLITE), testTable) { val dtType = currentDialectTest.dataTypeProvider.dateTimeType() val varCharType = currentDialectTest.dataTypeProvider.varcharType(100) val q = db.identifierManager.quoteString val baseExpression = "CREATE TABLE " + addIfNotExistsIfSupported() + "${"t".inProperCase()} (" + "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerAutoincType()} PRIMARY KEY, " + - "${"s".inProperCase()} $varCharType DEFAULT 'test' NOT NULL, " + - "${"sn".inProperCase()} $varCharType DEFAULT 'testNullable' NULL, " + - "${"l".inProperCase()} ${currentDialectTest.dataTypeProvider.longType()} DEFAULT 42 NOT NULL, " + - "$q${"c".inProperCase()}$q CHAR DEFAULT 'X' NOT NULL, " + - "${"t1".inProperCase()} $dtType ${currentDT.itOrNull()}, " + - "${"t2".inProperCase()} $dtType ${nowExpression.itOrNull()}, " + - "${"t3".inProperCase()} $dtType ${dtLiteral.itOrNull()}, " + - "${"t4".inProperCase()} DATE ${dtLiteral.itOrNull()}" + + "${"s".inProperCase()} $varCharType${testTable.s.constraintNamePart()} DEFAULT 'test' NOT NULL, " + + "${"sn".inProperCase()} $varCharType${testTable.sn.constraintNamePart()} DEFAULT 'testNullable' NULL, " + + "${"l".inProperCase()} ${currentDialectTest.dataTypeProvider.longType()}${testTable.l.constraintNamePart()} DEFAULT 42 NOT NULL, " + + "$q${"c".inProperCase()}$q CHAR${testTable.c.constraintNamePart()} DEFAULT 'X' NOT NULL, " + + "${"t1".inProperCase()} $dtType${testTable.t1.constraintNamePart()} ${currentDT.itOrNull()}, " + + "${"t2".inProperCase()} $dtType${testTable.t2.constraintNamePart()} ${nowExpression.itOrNull()}, " + + "${"t3".inProperCase()} $dtType${testTable.t3.constraintNamePart()} ${dtLiteral.itOrNull()}, " + + "${"t4".inProperCase()} DATE${testTable.t4.constraintNamePart()} ${dtLiteral.itOrNull()}" + ")" 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) } - assertEqualLists(expected, TestTable.ddl) + assertEqualLists(expected, testTable.ddl) - val id1 = TestTable.insertAndGetId { } + val id1 = testTable.insertAndGetId { } - val row1 = TestTable.select { TestTable.id eq id1 }.single() - assertEquals("test", row1[TestTable.s]) - assertEquals("testNullable", row1[TestTable.sn]) - assertEquals(42, row1[TestTable.l]) - assertEquals('X', row1[TestTable.c]) - assertEqualDateTime(dtConstValue.withTimeAtStartOfDay(), row1[TestTable.t3].withTimeAtStartOfDay()) - assertEqualDateTime(dtConstValue.withTimeAtStartOfDay(), row1[TestTable.t4].withTimeAtStartOfDay()) + val row1 = testTable.select { testTable.id eq id1 }.single() + assertEquals("test", row1[testTable.s]) + assertEquals("testNullable", row1[testTable.sn]) + assertEquals(42, row1[testTable.l]) + assertEquals('X', row1[testTable.c]) + assertEqualDateTime(dtConstValue.withTimeAtStartOfDay(), row1[testTable.t3].withTimeAtStartOfDay()) + assertEqualDateTime(dtConstValue.withTimeAtStartOfDay(), row1[testTable.t4].withTimeAtStartOfDay()) - val id2 = TestTable.insertAndGetId { it[TestTable.sn] = null } + val id2 = testTable.insertAndGetId { it[testTable.sn] = null } - TestTable.select { TestTable.id eq id2 }.single() + testTable.select { testTable.id eq id2 }.single() } } @Test fun testDefaultExpressions01() { - fun abs(value: Int) = object : ExpressionWithColumnType() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("ABS($value)") } @@ -275,39 +282,39 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { } @Test - fun defaultCurrentDateTimeTest() { - val TestDate = object : IntIdTable("TestDate") { + fun testDefaultCurrentDateTime() { + val testDate = object : IntIdTable("TestDate") { val time = datetime("time").defaultExpression(CurrentDateTime) } - withTables(TestDate) { - val duration: Long = 2_000 + withTables(testDate) { + val duration: Long = 2000 val before = currentDateTime() Thread.sleep(duration) repeat(2) { - TestDate.insertAndWait(duration) + testDate.insertAndWait(duration) } val middle = currentDateTime() Thread.sleep(duration) repeat(2) { - TestDate.insertAndWait(duration) + testDate.insertAndWait(duration) } val after = currentDateTime() - assertEquals(0, TestDate.select { TestDate.time less before }.count()) - assertEquals(4, TestDate.select { TestDate.time greater before }.count()) - assertEquals(2, TestDate.select { TestDate.time less middle }.count()) - assertEquals(2, TestDate.select { TestDate.time greater middle }.count()) - assertEquals(4, TestDate.select { TestDate.time less after }.count()) - assertEquals(0, TestDate.select { TestDate.time greater after }.count()) + assertEquals(0, testDate.select { testDate.time less before }.count()) + assertEquals(4, testDate.select { testDate.time greater before }.count()) + assertEquals(2, testDate.select { testDate.time less middle }.count()) + assertEquals(2, testDate.select { testDate.time greater middle }.count()) + assertEquals(4, testDate.select { testDate.time less after }.count()) + assertEquals(0, testDate.select { testDate.time greater after }.count()) } } // Checks that old numeric datetime columns works fine with new text representation @Test fun testSQLiteDateTimeFieldRegression() { - val TestDate = object : IntIdTable("TestDate") { + val testDate = object : IntIdTable("TestDate") { val time = datetime("time").defaultExpression(CurrentDateTime) } @@ -316,14 +323,14 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { exec( "CREATE TABLE IF NOT EXISTS TestDate (id INTEGER PRIMARY KEY AUTOINCREMENT, \"time\" NUMERIC DEFAULT (CURRENT_TIMESTAMP) NOT NULL);" ) - TestDate.insert { } - val year = TestDate.time.year() - val month = TestDate.time.month() - val day = TestDate.time.day() - val hour = TestDate.time.hour() - val minute = TestDate.time.minute() + testDate.insert { } + val year = testDate.time.year() + val month = testDate.time.month() + val day = testDate.time.day() + val hour = testDate.time.hour() + val minute = testDate.time.minute() - val result = TestDate.slice(year, month, day, hour, minute).selectAll().single() + val result = testDate.slice(year, month, day, hour, minute).selectAll().single() val now = DateTime.now() assertEquals(now.year, result[year]) @@ -332,32 +339,107 @@ class JodaTimeDefaultsTest : JodaTimeBaseTest() { assertEquals(now.hourOfDay, result[hour]) assertEquals(now.minuteOfHour, result[minute]) } finally { - SchemaUtils.drop(TestDate) + SchemaUtils.drop(testDate) } } } @Test fun `test No transaction in context when accessing datetime field outside the transaction`() { - val TestData = object : IntIdTable("TestData") { + val testData = object : IntIdTable("TestData") { val name = varchar("name", length = 50) val dateTime = datetime("date-time") } val date = DateTime.now() var list1: ResultRow? = null - withTables(TestData) { - TestData.insert { + withTables(testData) { + testData.insert { it[name] = "test1" it[dateTime] = date } - list1 = assertNotNull(TestData.selectAll().singleOrNull()) - assertEquals("test1", list1?.get(TestData.name)) - assertEquals(date.millis, list1?.get(TestData.dateTime)?.millis) + list1 = assertNotNull(testData.selectAll().singleOrNull()) + assertEquals("test1", list1?.get(testData.name)) + assertEquals(date.millis, list1?.get(testData.dateTime)?.millis) + } + assertEquals("test1", list1?.get(testData.name)) + assertEquals(date.millis, list1?.get(testData.dateTime)?.millis) + } + + @Test + fun testTimestampWithTimeZoneDefaults() { + // UTC time zone + DateTimeZone.setDefault(DateTimeZone.UTC) + assertEquals("UTC", DateTimeZone.getDefault().id) + + val nowWithTimeZone = DateTime.now() + val timestampWithTimeZoneLiteral = timestampWithTimeZoneLiteral(nowWithTimeZone) + + val testTable = object : IntIdTable("t") { + val t1 = timestampWithTimeZone("t1").default(nowWithTimeZone) + val t2 = timestampWithTimeZone("t2").defaultExpression(timestampWithTimeZoneLiteral) + } + + fun Expression<*>.itOrNull() = when { + currentDialectTest.isAllowedAsColumnDefault(this) -> + "DEFAULT ${currentDialectTest.dataTypeProvider.processForDefaultValue(this)} NOT NULL" + + else -> "NULL" + } + + withDb(excludeSettings = listOf(TestDB.SQLITE, TestDB.MARIADB)) { + if (!isOldMySql()) { + SchemaUtils.create(testTable) + + val timestampWithTimeZoneType = currentDialectTest.dataTypeProvider.timestampWithTimeZoneType() + + val baseExpression = "CREATE TABLE " + addIfNotExistsIfSupported() + + "${"t".inProperCase()} (" + + "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerAutoincType()} PRIMARY KEY, " + + "${"t1".inProperCase()} $timestampWithTimeZoneType${testTable.t1.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " + + "${"t2".inProperCase()} $timestampWithTimeZoneType${testTable.t2.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}" + + ")" + + 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 + ) + } else { + arrayListOf(baseExpression) + } + + assertEqualLists(expected, testTable.ddl) + + val id1 = testTable.insertAndGetId { } + + val row1 = testTable.select { testTable.id eq id1 }.single() + assertEqualDateTime(nowWithTimeZone, row1[testTable.t1]) + assertEqualDateTime(nowWithTimeZone, row1[testTable.t2]) + } + } + } + + @Test + fun testConsistentSchemeWithFunctionAsDefaultExpression() { + val foo = object : IntIdTable("foo") { + val name = text("name") + val defaultDateTime = datetime("defaultDateTime").defaultExpression(CurrentDateTime) + } + + withDb { + try { + SchemaUtils.create(foo) + + val actual = SchemaUtils.statementsRequiredToActualizeScheme(foo) + assertTrue(actual.isEmpty()) + } finally { + SchemaUtils.drop(foo) + } } - assertEquals("test1", list1?.get(TestData.name)) - assertEquals(date.millis, list1?.get(TestData.dateTime)?.millis) } } diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt index 3641c1adf6..7a413e7ed4 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeMiscTableTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("MaximumLineLength", "Wrapping") + package org.jetbrains.exposed import org.jetbrains.exposed.sql.* diff --git a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt index 21ea0076ff..cbcd7b0bc7 100644 --- a/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt +++ b/exposed-jodatime/src/test/kotlin/org/jetbrains/exposed/JodaTimeTests.kt @@ -1,15 +1,25 @@ package org.jetbrains.exposed +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.like import org.jetbrains.exposed.sql.jodatime.* +import org.jetbrains.exposed.sql.json.extract +import org.jetbrains.exposed.sql.json.jsonb 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.tests.shared.assertTrue +import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.* import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -177,6 +187,166 @@ open class JodaTimeBaseTest : DatabaseTestsBase() { assertEquals(2, createdIn2023.size) } } + + @Test + fun testLocalDateTimeComparison() { + val testTableDT = object : IntIdTable("test_table_dt") { + val created = datetime("created") + val modified = datetime("modified") + } + + withTables(testTableDT) { + val mayTheFourthDT = DateTime.parse("2011-05-04T13:00:21.871130789Z") + val nowDT = DateTime.now() + val id1 = testTableDT.insertAndGetId { + it[created] = mayTheFourthDT + it[modified] = mayTheFourthDT + } + val id2 = testTableDT.insertAndGetId { + it[created] = mayTheFourthDT + it[modified] = nowDT + } + + val createdMayFourth = testTableDT.select { testTableDT.created eq dateTimeParam(mayTheFourthDT) }.count() + assertEquals(2, createdMayFourth) + + val modifiedAtSameDT = testTableDT.select { testTableDT.modified eq testTableDT.created }.single() + assertEquals(id1, modifiedAtSameDT[testTableDT.id]) + + val modifiedAtLaterDT = testTableDT.select { testTableDT.modified greater testTableDT.created }.single() + assertEquals(id2, modifiedAtLaterDT[testTableDT.id]) + } + } + + @Test + fun testDateTimeAsJsonB() { + val tester = object : Table("tester") { + val created = datetime("created") + val modified = jsonb("modified", Json.Default) + } + + withTables(excludeSettings = TestDB.allH2TestDB + TestDB.SQLITE + TestDB.SQLSERVER + TestDB.ORACLE, tester) { + val dateTimeNow = DateTime.now() + tester.insert { + it[created] = dateTimeNow.minusYears(1) + it[modified] = ModifierData(1, dateTimeNow) + } + tester.insert { + it[created] = dateTimeNow.plusYears(1) + it[modified] = ModifierData(2, dateTimeNow) + } + + val prefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + + // value extracted in same manner it is stored, a json string + val modifiedAsString = tester.modified.extract("${prefix}timestamp") + val allModifiedAsString = tester.slice(modifiedAsString).selectAll() + assertTrue(allModifiedAsString.all { it[modifiedAsString] == dateTimeNow.toString() }) + + // PostgreSQL requires explicit type cast to timestamp for in-DB comparison + val dateModified = if (currentDialectTest is PostgreSQLDialect) { + tester.modified.extract("${prefix}timestamp").castTo(DateColumnType(true)) + } else { + tester.modified.extract("${prefix}timestamp") + } + val modifiedBeforeCreation = tester.select { dateModified less tester.created }.single() + assertEquals(2, modifiedBeforeCreation[tester.modified].userId) + } + } + + @Test + fun testTimestampWithTimeZone() { + val testTable = object : IntIdTable("TestTable") { + val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column") + } + + withDb(excludeSettings = listOf(TestDB.MARIADB)) { testDB -> + if (!isOldMySql()) { + SchemaUtils.create(testTable) + + // Cairo time zone + DateTimeZone.setDefault(DateTimeZone.forID("Africa/Cairo")) + assertEquals("Africa/Cairo", DateTimeZone.getDefault().id) + + val cairoNow = DateTime.now(DateTimeZone.getDefault()) + + val cairoId = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInCairoTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + // UTC time zone + DateTimeZone.setDefault(DateTimeZone.UTC) + assertEquals("UTC", DateTimeZone.getDefault().id) + + val cairoNowRetrievedInUTCTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + val utcID = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInUTCTimeZone = testTable.select { testTable.id eq utcID } + .single()[testTable.timestampWithTimeZone] + + // Tokyo time zone + DateTimeZone.setDefault(DateTimeZone.forID("Asia/Tokyo")) + assertEquals("Asia/Tokyo", DateTimeZone.getDefault().id) + + val cairoNowRetrievedInTokyoTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + val tokyoID = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInTokyoTimeZone = testTable.select { testTable.id eq tokyoID } + .single()[testTable.timestampWithTimeZone] + + // PostgreSQL and MySQL always store the timestamp in UTC, thereby losing the original time zone. + // To preserve the original time zone, store the time zone information in a separate column. + val isOriginalTimeZonePreserved = testDB !in listOf( + TestDB.POSTGRESQL, + TestDB.POSTGRESQLNG, + TestDB.MYSQL + ) + if (isOriginalTimeZonePreserved) { + // Assert that time zone is preserved when the same value is inserted in different time zones + assertEqualDateTime(cairoNow, cairoNowInsertedInCairoTimeZone) + assertEqualDateTime(cairoNow, cairoNowInsertedInUTCTimeZone) + assertEqualDateTime(cairoNow, cairoNowInsertedInTokyoTimeZone) + + // Assert that time zone is preserved when the same record is retrieved in different time zones + assertEqualDateTime(cairoNow, cairoNowRetrievedInUTCTimeZone) + assertEqualDateTime(cairoNow, cairoNowRetrievedInTokyoTimeZone) + } else { + // Assert equivalence in UTC when the same value is inserted in different time zones + assertEqualDateTime(cairoNowInsertedInCairoTimeZone, cairoNowInsertedInUTCTimeZone) + assertEqualDateTime(cairoNowInsertedInUTCTimeZone, cairoNowInsertedInTokyoTimeZone) + + // Assert equivalence in UTC when the same record is retrieved in different time zones + assertEqualDateTime(cairoNowRetrievedInUTCTimeZone, cairoNowRetrievedInTokyoTimeZone) + } + } + } + } + + @Test + fun testTimestampWithTimeZoneThrowsExceptionForUnsupportedDialects() { + val testTable = object : IntIdTable("TestTable") { + val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column") + } + + withDb(db = listOf(TestDB.MYSQL, TestDB.MARIADB)) { testDB -> + if (testDB == TestDB.MARIADB || isOldMySql()) { + expectException { + SchemaUtils.create(testTable) + } + } + } + } } fun assertEqualDateTime(d1: DateTime?, d2: DateTime?) { @@ -195,9 +365,22 @@ fun equalDateTime(d1: DateTime?, d2: DateTime?) = try { false } -val today: DateTime = DateTime.now().withTimeAtStartOfDay() +val today: DateTime = DateTime.now(DateTimeZone.UTC).withTimeAtStartOfDay() object CitiesTime : IntIdTable("CitiesTime") { val name = varchar("name", 50) // Column val local_time = datetime("local_time").nullable() // Column } + +@Serializable +data class ModifierData( + val userId: Int, + @Serializable(with = DateTimeSerializer::class) + val timestamp: DateTime +) + +object DateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DateTime", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: DateTime) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): DateTime = DateTime.parse(decoder.decodeString()) +} diff --git a/exposed-json/api/exposed-json.api b/exposed-json/api/exposed-json.api new file mode 100644 index 0000000000..4a65d55fd6 --- /dev/null +++ b/exposed-json/api/exposed-json.api @@ -0,0 +1,64 @@ +public final class org/jetbrains/exposed/sql/json/Contains : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getCandidate ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getJsonType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getPath ()Ljava/lang/String; + public final fun getTarget ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/json/Exists : org/jetbrains/exposed/sql/Op, org/jetbrains/exposed/sql/ComplexExpression { + public fun (Lorg/jetbrains/exposed/sql/Expression;[Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getJsonType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getOptional ()Ljava/lang/String; + public final fun getPath ()[Ljava/lang/String; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/json/Extract : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;[Ljava/lang/String;ZLorg/jetbrains/exposed/sql/IColumnType;Lorg/jetbrains/exposed/sql/IColumnType;)V + public final fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public final fun getJsonType ()Lorg/jetbrains/exposed/sql/IColumnType; + public final fun getPath ()[Ljava/lang/String; + public final fun getToScalar ()Z + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/json/JsonBColumnType : org/jetbrains/exposed/sql/json/JsonColumnType { + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public fun getUsesBinaryFormat ()Z + public fun sqlType ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/json/JsonBColumnTypeKt { + public static final fun jsonb (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Column; +} + +public class org/jetbrains/exposed/sql/json/JsonColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/JsonColumnMarker { + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public final fun getDeserialize ()Lkotlin/jvm/functions/Function1; + public final fun getSerialize ()Lkotlin/jvm/functions/Function1; + public fun getUsesBinaryFormat ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public synthetic fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/String; + public fun setParameter (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;ILjava/lang/Object;)V + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueToString (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class org/jetbrains/exposed/sql/json/JsonColumnTypeKt { + public static final fun json (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/json/JsonConditionsKt { + public static final fun contains (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/json/Contains; + public static final fun contains (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/json/Contains; + public static synthetic fun contains$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/json/Contains; + public static synthetic fun contains$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/json/Contains; + public static final fun exists (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;[Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/json/Exists; + public static synthetic fun exists$default (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;[Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/json/Exists; +} + diff --git a/exposed-json/build.gradle.kts b/exposed-json/build.gradle.kts new file mode 100644 index 0000000000..192c010c09 --- /dev/null +++ b/exposed-json/build.gradle.kts @@ -0,0 +1,24 @@ +import org.jetbrains.exposed.gradle.Versions + +plugins { + kotlin("jvm") apply true + kotlin("plugin.serialization") apply true +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(8) +} + +dependencies { + api(project(":exposed-core")) + api("org.jetbrains.kotlinx", "kotlinx-serialization-json", Versions.kotlinxSerialization) + compileOnly("org.postgresql", "postgresql", Versions.postgre) + testImplementation(project(":exposed-dao")) + testImplementation(project(":exposed-tests")) + testImplementation("junit", "junit", "4.12") + testImplementation(kotlin("test-junit")) +} diff --git a/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonBColumnType.kt b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonBColumnType.kt new file mode 100644 index 0000000000..c04eba40f6 --- /dev/null +++ b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonBColumnType.kt @@ -0,0 +1,60 @@ +package org.jetbrains.exposed.sql.json + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.currentDialect + +/** + * Column for storing JSON data in binary format. + * + * @param serialize Function that encodes an object of type [T] to a JSON String + * @param deserialize Function that decodes a JSON String to an object of type [T] + */ +class JsonBColumnType( + serialize: (T) -> String, + deserialize: (String) -> T +) : JsonColumnType(serialize, deserialize) { + override val usesBinaryFormat: Boolean = true + + override fun sqlType(): String = when (currentDialect) { + is H2Dialect -> (currentDialect as H2Dialect).originalDataTypeProvider.jsonBType() + else -> currentDialect.dataTypeProvider.jsonBType() + } +} + +/** + * Creates a column, with the specified [name], for storing JSON data in decomposed binary format. + * + * **Note**: JSON storage in binary format is not supported by all vendors; please check the documentation. + * + * @param name Name of the column + * @param serialize Function that encodes an object of type [T] to a JSON String + * @param deserialize Function that decodes a JSON string to an object of type [T] + */ +fun Table.jsonb( + name: String, + serialize: (T) -> String, + deserialize: (String) -> T +): Column = + registerColumn(name, JsonBColumnType(serialize, deserialize)) + +/** + * Creates a column, with the specified [name], for storing JSON data in decomposed binary format. + * + * **Note**: JSON storage in binary format is not supported by all vendors; please check the documentation. + * + * @param name Name of the column + * @param jsonConfig Configured instance of the `Json` class + * @param kSerializer Serializer responsible for the representation of a serial form of type [T]. + * Defaults to a generic serializer for type [T] + */ +inline fun Table.jsonb( + name: String, + jsonConfig: Json, + kSerializer: KSerializer = serializer() +): Column = + jsonb(name, { jsonConfig.encodeToString(kSerializer, it) }, { jsonConfig.decodeFromString(kSerializer, it) }) diff --git a/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonColumnType.kt b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonColumnType.kt new file mode 100644 index 0000000000..a74541ad57 --- /dev/null +++ b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonColumnType.kt @@ -0,0 +1,101 @@ +package org.jetbrains.exposed.sql.json + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.ColumnType +import org.jetbrains.exposed.sql.JsonColumnMarker +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi +import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect +import org.jetbrains.exposed.sql.vendors.currentDialect +import org.postgresql.util.PGobject + +/** + * Column for storing JSON data, either in non-binary text format or the vendor's default JSON type format. + */ +open class JsonColumnType( + /** Returns the function that encodes an object of type [T] to a JSON String. */ + val serialize: (T) -> String, + /** Returns the function that decodes a JSON String to an object of type [T]. */ + val deserialize: (String) -> T +) : ColumnType(), JsonColumnMarker { + override val usesBinaryFormat: Boolean = false + + override fun sqlType(): String = currentDialect.dataTypeProvider.jsonType() + + override fun valueFromDB(value: Any): Any { + return when { + currentDialect is PostgreSQLDialect && value is PGobject -> deserialize(value.value!!) + value is String -> deserialize(value) + value is ByteArray -> deserialize(value.decodeToString()) + else -> value + } + } + + @Suppress("UNCHECKED_CAST") + override fun notNullValueToDB(value: Any) = serialize(value as T) + + override fun valueToString(value: Any?): String = when (value) { + is Iterable<*> -> nonNullValueToString(value) + else -> super.valueToString(value) + } + + override fun nonNullValueToString(value: Any): String { + return when (currentDialect) { + is H2Dialect -> "JSON '${notNullValueToDB(value)}'" + else -> super.nonNullValueToString(value) + } + } + + override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { + val parameterValue = when (currentDialect) { + is PostgreSQLDialect -> PGobject().apply { + type = sqlType() + this.value = value as String? + } + is H2Dialect -> (value as String).encodeToByteArray() + else -> value + } + super.setParameter(stmt, index, parameterValue) + } +} + +/** + * Creates a column, with the specified [name], for storing JSON data. + * + * **Note**: This column stores JSON either in non-binary text format or, + * if the vendor only supports 1 format, the default JSON type format. + * If JSON must be stored in binary format, and the vendor supports this, please use `jsonb()` instead. + * + * @param name Name of the column + * @param serialize Function that encodes an object of type [T] to a JSON String + * @param deserialize Function that decodes a JSON string to an object of type [T] + */ +fun Table.json( + name: String, + serialize: (T) -> String, + deserialize: (String) -> T +): Column = + registerColumn(name, JsonColumnType(serialize, deserialize)) + +/** + * Creates a column, with the specified [name], for storing JSON data. + * + * **Note**: This column stores JSON either in non-binary text format or, + * if the vendor only supports 1 format, the default JSON type format. + * If JSON must be stored in binary format, and the vendor supports this, please use `jsonb()` instead. + * + * @param name Name of the column + * @param jsonConfig Configured instance of the `Json` class + * @param kSerializer Serializer responsible for the representation of a serial form of type [T]. + * Defaults to a generic serializer for type [T] + */ +inline fun Table.json( + name: String, + jsonConfig: Json, + kSerializer: KSerializer = serializer() +): Column = + json(name, { jsonConfig.encodeToString(kSerializer, it) }, { jsonConfig.decodeFromString(kSerializer, it) }) diff --git a/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt new file mode 100644 index 0000000000..17fffebdfc --- /dev/null +++ b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonConditions.kt @@ -0,0 +1,80 @@ +package org.jetbrains.exposed.sql.json + +import org.jetbrains.exposed.sql.ComplexExpression +import org.jetbrains.exposed.sql.Expression +import org.jetbrains.exposed.sql.ExpressionWithColumnType +import org.jetbrains.exposed.sql.IColumnType +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.QueryBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.asLiteral +import org.jetbrains.exposed.sql.vendors.currentDialect + +// Operator Classes + +/** + * Represents an SQL operator that checks whether a [candidate] expression is contained within a JSON [target]. + */ +class Contains( + /** Returns the JSON expression being searched. */ + val target: Expression<*>, + /** Returns the expression being searched for in [target]. */ + val candidate: Expression<*>, + /** Returns an optional String representing JSON path/keys that match specific fields to search for [candidate]. */ + val path: String?, + /** Returns the column type of [target] to check, if casting to JSONB is required. */ + val jsonType: IColumnType +) : Op(), ComplexExpression { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = + currentDialect.functionProvider.jsonContains(target, candidate, path, jsonType, queryBuilder) +} + +/** + * Represents an SQL operator that checks whether data exists within a JSON [expression] at the specified [path]. + */ +class Exists( + /** Returns the JSON expression being checked. */ + val expression: Expression<*>, + /** Returns the array of Strings representing JSON path/keys that match fields to check for existing data. */ + vararg val path: String, + /** Returns an optional String representing any vendor-specific clause or argument. */ + val optional: String?, + /** Returns the column type of [expression] to check, if casting to JSONB is required. */ + val jsonType: IColumnType +) : Op(), ComplexExpression { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = + currentDialect.functionProvider.jsonExists(expression, path = path, optional, jsonType, queryBuilder) +} + +// Extension Functions + +/** + * Checks whether a [candidate] expression is contained within [this] JSON expression. + * + * @param candidate Expression to search for in [this] JSON expression. + * @param path String representing JSON path/keys that match specific fields to search for [candidate]. + * **Note:** Optional [path] argument is not supported by all vendors; please check the documentation. + */ +fun ExpressionWithColumnType<*>.contains(candidate: Expression<*>, path: String? = null): Contains = + Contains(this, candidate, path, columnType) + +/** + * Checks whether a [candidate] value is contained within [this] JSON expression. + * + * @param candidate Value to search for in [this] JSON expression. + * @param path String representing JSON path/keys that match specific fields to search for [candidate]. + * **Note:** Optional [path] argument is not supported by all vendors; please check the documentation. + */ +fun ExpressionWithColumnType<*>.contains(candidate: T, path: String? = null): Contains = + Contains(this, asLiteral(candidate), path, columnType) + +/** + * Checks whether data exists within [this] JSON expression at the specified [path]. + * + * @param path String(s) representing JSON path/keys that match fields to check for existing data. + * If none are provided, the root context item `'$'` will be used by default. + * **Note:** Multiple [path] arguments are not supported by all vendors; please check the documentation. + * @param optional String representing any optional vendor-specific clause or argument. + * **Note:** [optional] function arguments are not supported by all vendors; please check the documentation. + */ +fun ExpressionWithColumnType<*>.exists(vararg path: String, optional: String? = null): Exists = + Exists(this, path = path, optional, columnType) diff --git a/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonFunctions.kt b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonFunctions.kt new file mode 100644 index 0000000000..84de402c6e --- /dev/null +++ b/exposed-json/src/main/kotlin/org/jetbrains/exposed/sql/json/JsonFunctions.kt @@ -0,0 +1,64 @@ +@file: Suppress("MatchingDeclarationName") + +package org.jetbrains.exposed.sql.json + +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Function +import org.jetbrains.exposed.sql.vendors.currentDialect + +// Function Classes + +/** + * Represents an SQL function that returns extracted data from a JSON object at the specified [path], + * either as a JSON representation or as a scalar value. + */ +class Extract( + /** Returns the expression from which to extract JSON subcomponents matched by [path]. */ + val expression: Expression<*>, + /** Returns array of Strings representing JSON path/keys that match fields to be extracted. */ + vararg val path: String, + /** Returns whether the extracted result should be a scalar or text value; if `false`, result will be a JSON object. */ + val toScalar: Boolean, + /** Returns the column type of [expression] to check, if casting to JSONB is required. */ + val jsonType: IColumnType, + columnType: IColumnType +) : Function(columnType) { + override fun toQueryBuilder(queryBuilder: QueryBuilder) = + currentDialect.functionProvider.jsonExtract(expression, path = path, toScalar, jsonType, queryBuilder) +} + +// Extension Functions + +/** + * Returns the extracted data from a JSON object at the specified [path], either as a JSON representation or as a scalar value. + * + * @param path String(s) representing JSON path/keys that match fields to be extracted. + * If none are provided, the root context item `'$'` will be used by default. + * **Note:** Multiple [path] arguments are not supported by all vendors; please check the documentation. + * @param toScalar If `true`, the extracted result is a scalar or text value; otherwise, it is a JSON object. + */ +inline fun ExpressionWithColumnType<*>.extract( + vararg path: String, + toScalar: Boolean = true +): Extract { + val columnType = when (T::class) { + String::class -> TextColumnType() + Boolean::class -> BooleanColumnType() + Long::class -> LongColumnType() + Int::class -> IntegerColumnType() + Short::class -> ShortColumnType() + Byte::class -> ByteColumnType() + Double::class -> DoubleColumnType() + Float::class -> FloatColumnType() + ByteArray::class -> BasicBinaryColumnType() + else -> { + JsonColumnType( + { Json.Default.encodeToString(serializer(), it) }, + { Json.Default.decodeFromString(serializer(), it) } + ) + } + } + return Extract(this, path = path, toScalar, this.columnType, columnType) +} diff --git a/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonBColumnTests.kt b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonBColumnTests.kt new file mode 100644 index 0000000000..6cbe09554d --- /dev/null +++ b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonBColumnTests.kt @@ -0,0 +1,309 @@ +package org.jetbrains.exposed.sql.json + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq +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.assertEqualCollections +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue +import org.jetbrains.exposed.sql.tests.shared.expectException +import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect +import org.junit.Test +import kotlin.test.assertContentEquals + +class JsonBColumnTests : DatabaseTestsBase() { + private val binaryJsonNotSupportedDB = listOf(TestDB.SQLITE, TestDB.SQLSERVER) + TestDB.ORACLE + + @Test + fun testInsertAndSelect() { + withJsonBTable(exclude = binaryJsonNotSupportedDB) { tester, _, _, _ -> + val newData = DataHolder(User("Pro", "Alpha"), 999, true, "A") + val newId = tester.insertAndGetId { + it[jsonBColumn] = newData + } + + val newResult = tester.select { tester.id eq newId }.singleOrNull() + assertEquals(newData, newResult?.get(tester.jsonBColumn)) + } + } + + @Test + fun testUpdate() { + withJsonBTable(exclude = binaryJsonNotSupportedDB) { tester, _, data1, _ -> + assertEquals(data1, tester.selectAll().single()[tester.jsonBColumn]) + + val updatedData = data1.copy(active = false) + tester.update { + it[jsonBColumn] = updatedData + } + + assertEquals(updatedData, tester.selectAll().single()[tester.jsonBColumn]) + } + } + + @Test + fun testSelectWithSliceExtract() { + withJsonBTable(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, user1, data1, _ -> + val pathPrefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + val isActive = tester.jsonBColumn.extract("${pathPrefix}active", toScalar = false) + val result1 = tester.slice(isActive).selectAll().singleOrNull() + assertEquals(data1.active, result1?.get(isActive)) + + val storedUser = tester.jsonBColumn.extract("${pathPrefix}user", toScalar = false) + val result2 = tester.slice(storedUser).selectAll().singleOrNull() + assertEquals(user1, result2?.get(storedUser)) + + val path = if (currentDialectTest is PostgreSQLDialect) arrayOf("user", "name") else arrayOf(".user.name") + val username = tester.jsonBColumn.extract(*path) + val result3 = tester.slice(username).selectAll().singleOrNull() + assertEquals(user1.name, result3?.get(username)) + } + } + + @Test + fun testSelectWhereWithExtract() { + withJsonBTable(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, _, data1, _ -> + val newId = tester.insertAndGetId { + it[jsonBColumn] = data1.copy(logins = 1000) + } + + // Postgres requires type casting to compare jsonb field as integer value in DB ??? + val logins = if (currentDialectTest is PostgreSQLDialect) { + tester.jsonBColumn.extract("logins").castTo(IntegerColumnType()) + } else { + tester.jsonBColumn.extract(".logins") + } + val tooManyLogins = logins greaterEq 1000 + + val result = tester.slice(tester.id).select { tooManyLogins }.singleOrNull() + assertEquals(newId, result?.get(tester.id)) + } + } + + @Test + fun testDAOFunctionsWithJsonBColumn() { + val dataTable = JsonTestsData.JsonBTable + val dataEntity = JsonTestsData.JsonBEntity + + withDb(excludeSettings = binaryJsonNotSupportedDB) { testDb -> + excludingH2Version1(testDb) { + SchemaUtils.create(dataTable) + + val dataA = DataHolder(User("Admin", "Alpha"), 10, true, null) + val newUser = dataEntity.new { + jsonBColumn = dataA + } + + assertEquals(dataA, dataEntity.findById(newUser.id)?.jsonBColumn) + + val updatedUser = dataA.copy(logins = 99) + dataTable.update { + it[jsonBColumn] = updatedUser + } + + assertEquals(updatedUser, dataEntity.all().single().jsonBColumn) + + if (testDb !in TestDB.allH2TestDB) { + dataEntity.new { jsonBColumn = dataA } + val loginCount = if (currentDialectTest is PostgreSQLDialect) { + dataTable.jsonBColumn.extract("logins").castTo(IntegerColumnType()) + } else { + dataTable.jsonBColumn.extract(".logins") + } + val frequentUser = dataEntity.find { loginCount greaterEq 50 }.single() + assertEquals(updatedUser, frequentUser.jsonBColumn) + } + + SchemaUtils.drop(dataTable) + } + } + } + + @Test + fun testJsonContains() { + withJsonBTable(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, user1, data1, testDb -> + val alphaTeamUser = user1.copy(team = "Alpha") + val newId = tester.insertAndGetId { + it[jsonBColumn] = data1.copy(user = alphaTeamUser) + } + + val userIsInactive = tester.jsonBColumn.contains("{\"active\":false}") + assertEquals(0, tester.select { userIsInactive }.count()) + + val alphaTeamUserAsJson = "{\"user\":${Json.Default.encodeToString(alphaTeamUser)}}" + var userIsInAlphaTeam = tester.jsonBColumn.contains(stringLiteral(alphaTeamUserAsJson)) + assertEquals(1, tester.select { userIsInAlphaTeam }.count()) + + // test target contains candidate at specified path + if (testDb in TestDB.mySqlRelatedDB) { + userIsInAlphaTeam = tester.jsonBColumn.contains("\"Alpha\"", ".user.team") + val alphaTeamUsers = tester.slice(tester.id).select { userIsInAlphaTeam } + assertEquals(newId, alphaTeamUsers.single()[tester.id]) + } + } + } + + @Test + fun testJsonExists() { + withJsonBTable(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, _, data1, testDb -> + val maximumLogins = 1000 + val teamA = "A" + val newId = tester.insertAndGetId { + it[jsonBColumn] = data1.copy(user = data1.user.copy(team = teamA), logins = maximumLogins) + } + + val optional = if (testDb in TestDB.mySqlRelatedDB) "one" else null + + // test data at path root '$' exists by providing no path arguments + val hasAnyData = tester.jsonBColumn.exists(optional = optional) + assertEquals(2, tester.select { hasAnyData }.count()) + + val hasFakeKey = tester.jsonBColumn.exists(".fakeKey", optional = optional) + assertEquals(0, tester.select { hasFakeKey }.count()) + + val hasLogins = tester.jsonBColumn.exists(".logins", optional = optional) + assertEquals(2, tester.select { hasLogins }.count()) + + // test data at path exists with filter condition & optional arguments + if (currentDialectTest is PostgreSQLDialect) { + val filterPath = ".logins ? (@ == $maximumLogins)" + val hasMaxLogins = tester.jsonBColumn.exists(filterPath) + val usersWithMaxLogin = tester.slice(tester.id).select { hasMaxLogins } + assertEquals(newId, usersWithMaxLogin.single()[tester.id]) + + val (jsonPath, optionalArg) = ".user.team ? (@ == \$team)" to "{\"team\":\"$teamA\"}" + val isOnTeamA = tester.jsonBColumn.exists(jsonPath, optional = optionalArg) + val usersOnTeamA = tester.slice(tester.id).select { isOnTeamA } + assertEquals(newId, usersOnTeamA.single()[tester.id]) + } + } + } + + @Test + fun testJsonExtractWithArrays() { + withJsonBArrays(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, singleId, _, _ -> + val path1 = if (currentDialectTest is PostgreSQLDialect) { + arrayOf("users", "0", "team") + } else { + arrayOf(".users[0].team") + } + val firstIsOnTeamA = tester.groups.extract(*path1) eq "Team A" + assertEquals(singleId, tester.select { firstIsOnTeamA }.single()[tester.id]) + + // older MySQL and MariaDB versions require non-scalar extracted value from JSON Array + val path2 = if (currentDialectTest is PostgreSQLDialect) "0" else "[0]" + val firstNumber = tester.numbers.extract(path2, toScalar = !isOldMySql()) + assertEqualCollections(listOf(100, 3), tester.slice(firstNumber).selectAll().map { it[firstNumber] }) + } + } + + @Test + fun testJsonContainsWithArrays() { + withJsonBArrays(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, _, tripleId, testDb -> + val hasSmallNumbers = tester.numbers.contains("[3, 5]") + assertEquals(tripleId, tester.select { hasSmallNumbers }.single()[tester.id]) + + if (testDb in TestDB.mySqlRelatedDB) { + val hasUserNameB = tester.groups.contains("\"B\"", ".users[0].name") + assertEquals(tripleId, tester.select { hasUserNameB }.single()[tester.id]) + } + } + } + + @Test + fun testJsonExistsWithArrays() { + withJsonBArrays(exclude = binaryJsonNotSupportedDB + TestDB.allH2TestDB) { tester, _, tripleId, testDb -> + val optional = if (testDb in TestDB.mySqlRelatedDB) "one" else null + + val hasMultipleUsers = tester.groups.exists(".users[1]", optional = optional) + assertEquals(tripleId, tester.select { hasMultipleUsers }.single()[tester.id]) + + val hasAtLeast3Numbers = tester.numbers.exists("[2]", optional = optional) + assertEquals(tripleId, tester.select { hasAtLeast3Numbers }.single()[tester.id]) + } + } + + @Test + fun testJsonBWithDefaults() { + val defaultUser = User("UNKNOWN", "UNASSIGNED") + val defaultTester = object : Table("default_tester") { + val user1 = jsonb("user_1", Json.Default).default(defaultUser) + val user2 = jsonb("user_2", Json.Default).clientDefault { defaultUser } + } + + withDb(excludeSettings = binaryJsonNotSupportedDB) { testDb -> + excludingH2Version1(testDb) { + if (isOldMySql()) { + expectException { + SchemaUtils.createMissingTablesAndColumns(defaultTester) + } + } else { + SchemaUtils.createMissingTablesAndColumns(defaultTester) + assertTrue(defaultTester.exists()) + // ensure defaults match returned metadata defaults + val alters = SchemaUtils.statementsRequiredToActualizeScheme(defaultTester) + assertTrue(alters.isEmpty()) + + defaultTester.insert {} + + defaultTester.selectAll().single().also { + assertEquals(defaultUser.name, it[defaultTester.user1].name) + assertEquals(defaultUser.team, it[defaultTester.user1].team) + + assertEquals(defaultUser.name, it[defaultTester.user2].name) + assertEquals(defaultUser.team, it[defaultTester.user2].team) + } + + SchemaUtils.drop(defaultTester) + } + } + } + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testLoggerWithJsonBCollections() { + val iterables = object : Table("iterables_tester") { + val userList = jsonb("user_list", Json.Default, ListSerializer(User.serializer())) + val intList = jsonb>("int_list", Json.Default) + val userArray = jsonb("user_array", Json.Default, ArraySerializer(User.serializer())) + val intArray = jsonb("int_array", Json.Default) + } + + withDb(excludeSettings = binaryJsonNotSupportedDB) { testDb -> + excludingH2Version1(testDb) { + // the logger is left in to test that it does not throw ClassCastException on insertion of iterables + addLogger(StdOutSqlLogger) + SchemaUtils.create(iterables) + + val user1 = User("A", "Team A") + val user2 = User("B", "Team B") + val integerList = listOf(1, 2, 3) + val integerArray = intArrayOf(1, 2, 3) + iterables.insert { + it[userList] = listOf(user1, user2) + it[intList] = integerList + it[userArray] = arrayOf(user1, user2) + it[intArray] = integerArray + } + + val result = iterables.selectAll().single() + assertEqualCollections(listOf(user1, user2), result[iterables.userList]) + assertEqualCollections(integerList, result[iterables.intList]) + assertContentEquals(arrayOf(user1, user2), result[iterables.userArray]) + assertContentEquals(integerArray, result[iterables.intArray]) + + SchemaUtils.drop(iterables) + } + } + } +} diff --git a/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt new file mode 100644 index 0000000000..887275fe71 --- /dev/null +++ b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonColumnTests.kt @@ -0,0 +1,344 @@ +package org.jetbrains.exposed.sql.json + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq +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.assertEqualCollections +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue +import org.jetbrains.exposed.sql.tests.shared.expectException +import org.jetbrains.exposed.sql.vendors.OracleDialect +import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect +import org.jetbrains.exposed.sql.vendors.SQLServerDialect +import org.junit.Test +import kotlin.test.assertContentEquals + +class JsonColumnTests : DatabaseTestsBase() { + @Test + fun testInsertAndSelect() { + withJsonTable { tester, _, _, _ -> + val newData = DataHolder(User("Pro", "Alpha"), 999, true, "A") + val newId = tester.insertAndGetId { + it[jsonColumn] = newData + } + + val newResult = tester.select { tester.id eq newId }.singleOrNull() + assertEquals(newData, newResult?.get(tester.jsonColumn)) + } + } + + @Test + fun testUpdate() { + withJsonTable { tester, _, data1, _ -> + assertEquals(data1, tester.selectAll().single()[tester.jsonColumn]) + + val updatedData = data1.copy(active = false) + tester.update { + it[jsonColumn] = updatedData + } + + assertEquals(updatedData, tester.selectAll().single()[tester.jsonColumn]) + } + } + + @Test + fun testSelectWithSliceExtract() { + withJsonTable(exclude = TestDB.allH2TestDB) { tester, user1, data1, _ -> + val pathPrefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + // SQLServer & Oracle return null if extracted JSON is not scalar + val requiresScalar = currentDialectTest is SQLServerDialect || currentDialectTest is OracleDialect + val isActive = tester.jsonColumn.extract("${pathPrefix}active", toScalar = requiresScalar) + val result1 = tester.slice(isActive).selectAll().singleOrNull() + assertEquals(data1.active, result1?.get(isActive)) + + val storedUser = tester.jsonColumn.extract("${pathPrefix}user", toScalar = false) + val result2 = tester.slice(storedUser).selectAll().singleOrNull() + assertEquals(user1, result2?.get(storedUser)) + + val path = if (currentDialectTest is PostgreSQLDialect) arrayOf("user", "name") else arrayOf(".user.name") + val username = tester.jsonColumn.extract(*path) + val result3 = tester.slice(username).selectAll().singleOrNull() + assertEquals(user1.name, result3?.get(username)) + } + } + + @Test + fun testSelectWhereWithExtract() { + withJsonTable(exclude = TestDB.allH2TestDB) { tester, _, data1, _ -> + val newId = tester.insertAndGetId { + it[jsonColumn] = data1.copy(logins = 1000) + } + + // Postgres requires type casting to compare json field as integer value in DB + val logins = if (currentDialectTest is PostgreSQLDialect) { + tester.jsonColumn.extract("logins").castTo(IntegerColumnType()) + } else { + tester.jsonColumn.extract(".logins") + } + val tooManyLogins = logins greaterEq 1000 + + val result = tester.slice(tester.id).select { tooManyLogins }.singleOrNull() + assertEquals(newId, result?.get(tester.id)) + } + } + + @Test + fun testWithNonSerializableClass() { + data class Fake(val number: Int) + + withDb { testDb -> + excludingH2Version1(testDb) { + expectException { + // Throws with message: Serializer for class 'Fake' is not found. + // Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied. + val tester = object : Table("tester") { + val jCol = json("j_col", Json) + } + } + } + } + } + + @Test + fun testDAOFunctionsWithJsonColumn() { + val dataTable = JsonTestsData.JsonTable + val dataEntity = JsonTestsData.JsonEntity + + withDb { testDb -> + excludingH2Version1(testDb) { + SchemaUtils.create(dataTable) + + val dataA = DataHolder(User("Admin", "Alpha"), 10, true, null) + val newUser = dataEntity.new { + jsonColumn = dataA + } + + assertEquals(dataA, dataEntity.findById(newUser.id)?.jsonColumn) + + val updatedUser = dataA.copy(user = User("Lead", "Beta")) + dataTable.update { + it[jsonColumn] = updatedUser + } + + assertEquals(updatedUser, dataEntity.all().single().jsonColumn) + + if (testDb !in TestDB.allH2TestDB) { + dataEntity.new { jsonColumn = dataA } + val path = if (currentDialectTest is PostgreSQLDialect) { + arrayOf("user", "team") + } else { + arrayOf(".user.team") + } + val userTeam = dataTable.jsonColumn.extract(*path) + val userInTeamB = dataEntity.find { userTeam like "B%" }.single() + + assertEquals(updatedUser, userInTeamB.jsonColumn) + } + + SchemaUtils.drop(dataTable) + } + } + } + + private val jsonContainsNotSupported = TestDB.values().toList() - + listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.MYSQL, TestDB.MARIADB) + + @Test + fun testJsonContains() { + withJsonTable(exclude = jsonContainsNotSupported) { tester, user1, data1, testDb -> + val alphaTeamUser = user1.copy(team = "Alpha") + val newId = tester.insertAndGetId { + it[jsonColumn] = data1.copy(user = alphaTeamUser) + } + + val userIsInactive = tester.jsonColumn.contains("{\"active\":false}") + val result = tester.select { userIsInactive }.toList() + assertEquals(0, result.size) + + val alphaTeamUserAsJson = "{\"user\":${Json.Default.encodeToString(alphaTeamUser)}}" + var userIsInAlphaTeam = tester.jsonColumn.contains(stringLiteral(alphaTeamUserAsJson)) + assertEquals(1, tester.select { userIsInAlphaTeam }.count()) + + // test target contains candidate at specified path + if (testDb in TestDB.mySqlRelatedDB) { + userIsInAlphaTeam = tester.jsonColumn.contains("\"Alpha\"", ".user.team") + val alphaTeamUsers = tester.slice(tester.id).select { userIsInAlphaTeam } + assertEquals(newId, alphaTeamUsers.single()[tester.id]) + } + } + } + + @Test + fun testJsonExists() { + withJsonTable(exclude = TestDB.allH2TestDB + TestDB.SQLSERVER) { tester, _, data1, testDb -> + val maximumLogins = 1000 + val teamA = "A" + val newId = tester.insertAndGetId { + it[jsonColumn] = data1.copy(user = data1.user.copy(team = teamA), logins = maximumLogins) + } + + val optional = if (testDb in TestDB.mySqlRelatedDB) "one" else null + + // test data at path root '$' exists by providing no path arguments + val hasAnyData = tester.jsonColumn.exists(optional = optional) + assertEquals(2, tester.select { hasAnyData }.count()) + + val hasFakeKey = tester.jsonColumn.exists(".fakeKey", optional = optional) + assertEquals(0, tester.select { hasFakeKey }.count()) + + val hasLogins = tester.jsonColumn.exists(".logins", optional = optional) + assertEquals(2, tester.select { hasLogins }.count()) + + // test data at path exists with filter condition & optional arguments + val testDialect = currentDialectTest + if (testDialect is OracleDialect || testDialect is PostgreSQLDialect) { + val filterPath = if (testDialect is OracleDialect) { + "?(@.logins == $maximumLogins)" + } else { + ".logins ? (@ == $maximumLogins)" + } + val hasMaxLogins = tester.jsonColumn.exists(filterPath) + val usersWithMaxLogin = tester.slice(tester.id).select { hasMaxLogins } + assertEquals(newId, usersWithMaxLogin.single()[tester.id]) + + val (jsonPath, optionalArg) = if (testDialect is OracleDialect) { + "?(@.user.team == \$team)" to "PASSING '$teamA' AS \"team\"" + } else { + ".user.team ? (@ == \$team)" to "{\"team\":\"$teamA\"}" + } + val isOnTeamA = tester.jsonColumn.exists(jsonPath, optional = optionalArg) + val usersOnTeamA = tester.slice(tester.id).select { isOnTeamA } + assertEquals(newId, usersOnTeamA.single()[tester.id]) + } + } + } + + @Test + fun testJsonExtractWithArrays() { + withJsonArrays(exclude = TestDB.allH2TestDB) { tester, singleId, _, _ -> + val path1 = if (currentDialectTest is PostgreSQLDialect) { + arrayOf("users", "0", "team") + } else { + arrayOf(".users[0].team") + } + val firstIsOnTeamA = tester.groups.extract(*path1) eq "Team A" + assertEquals(singleId, tester.select { firstIsOnTeamA }.single()[tester.id]) + + // older MySQL and MariaDB versions require non-scalar extracted value from JSON Array + val path2 = if (currentDialectTest is PostgreSQLDialect) "0" else "[0]" + val firstNumber = tester.numbers.extract(path2, toScalar = !isOldMySql()) + assertEqualCollections(listOf(100, 3), tester.slice(firstNumber).selectAll().map { it[firstNumber] }) + } + } + + @Test + fun testJsonContainsWithArrays() { + withJsonArrays(exclude = jsonContainsNotSupported) { tester, _, tripleId, testDb -> + val hasSmallNumbers = tester.numbers.contains("[3, 5]") + assertEquals(tripleId, tester.select { hasSmallNumbers }.single()[tester.id]) + + if (testDb in TestDB.mySqlRelatedDB) { + val hasUserNameB = tester.groups.contains("\"B\"", ".users[0].name") + assertEquals(tripleId, tester.select { hasUserNameB }.single()[tester.id]) + } + } + } + + @Test + fun testJsonExistsWithArrays() { + withJsonArrays(exclude = TestDB.allH2TestDB + TestDB.SQLSERVER) { tester, _, tripleId, testDb -> + val optional = if (testDb in TestDB.mySqlRelatedDB) "one" else null + + val hasMultipleUsers = tester.groups.exists(".users[1]", optional = optional) + assertEquals(tripleId, tester.select { hasMultipleUsers }.single()[tester.id]) + + val hasAtLeast3Numbers = tester.numbers.exists("[2]", optional = optional) + assertEquals(tripleId, tester.select { hasAtLeast3Numbers }.single()[tester.id]) + } + } + + @Test + fun testJsonWithDefaults() { + val defaultUser = User("UNKNOWN", "UNASSIGNED") + val defaultTester = object : Table("default_tester") { + val user1 = json("user_1", Json.Default).default(defaultUser) + val user2 = json("user_2", Json.Default).clientDefault { defaultUser } + } + + withDb { testDb -> + excludingH2Version1(testDb) { + if (isOldMySql()) { + expectException { + SchemaUtils.createMissingTablesAndColumns(defaultTester) + } + } else { + SchemaUtils.createMissingTablesAndColumns(defaultTester) + assertTrue(defaultTester.exists()) + // ensure defaults match returned metadata defaults + val alters = SchemaUtils.statementsRequiredToActualizeScheme(defaultTester) + assertTrue(alters.isEmpty()) + + defaultTester.insert {} + + defaultTester.selectAll().single().also { + assertEquals(defaultUser.name, it[defaultTester.user1].name) + assertEquals(defaultUser.team, it[defaultTester.user1].team) + + assertEquals(defaultUser.name, it[defaultTester.user2].name) + assertEquals(defaultUser.team, it[defaultTester.user2].team) + } + + SchemaUtils.drop(defaultTester) + } + } + } + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun testLoggerWithJsonCollections() { + val iterables = object : Table("iterables_tester") { + val userList = json("user_list", Json.Default, ListSerializer(User.serializer())) + val intList = json>("int_list", Json.Default) + val userArray = json("user_array", Json.Default, ArraySerializer(User.serializer())) + val intArray = json("int_array", Json.Default) + } + + withDb { testDb -> + excludingH2Version1(testDb) { + // the logger is left in to test that it does not throw ClassCastException on insertion of iterables + addLogger(StdOutSqlLogger) + SchemaUtils.create(iterables) + + val user1 = User("A", "Team A") + val user2 = User("B", "Team B") + val integerList = listOf(1, 2, 3) + val integerArray = intArrayOf(1, 2, 3) + iterables.insert { + it[userList] = listOf(user1, user2) + it[intList] = integerList + it[userArray] = arrayOf(user1, user2) + it[intArray] = integerArray + } + + val result = iterables.selectAll().single() + assertEqualCollections(listOf(user1, user2), result[iterables.userList]) + assertEqualCollections(integerList, result[iterables.intList]) + assertContentEquals(arrayOf(user1, user2), result[iterables.userArray]) + assertContentEquals(integerArray, result[iterables.intArray]) + + SchemaUtils.drop(iterables) + } + } + } +} diff --git a/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonTestsData.kt b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonTestsData.kt new file mode 100644 index 0000000000..986068f802 --- /dev/null +++ b/exposed-json/src/test/kotlin/org/jetbrains/exposed/sql/json/JsonTestsData.kt @@ -0,0 +1,161 @@ +package org.jetbrains.exposed.sql.json + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB + +object JsonTestsData { + object JsonTable : IntIdTable("j_table") { + val jsonColumn = json("j_column", Json.Default) + } + + object JsonBTable : IntIdTable("j_b_table") { + val jsonBColumn = jsonb("j_b_column", Json.Default) + } + + class JsonEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(JsonTable) + + var jsonColumn by JsonTable.jsonColumn + } + + class JsonBEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(JsonBTable) + + var jsonBColumn by JsonBTable.jsonBColumn + } + + object JsonArrays : IntIdTable("j_arrays") { + val groups = json("groups", Json.Default) + val numbers = json("numbers", Json.Default) + } + + object JsonBArrays : IntIdTable("j_b_arrays") { + val groups = jsonb("groups", Json.Default) + val numbers = jsonb("numbers", Json.Default) + } +} + +fun DatabaseTestsBase.withJsonTable( + exclude: List = emptyList(), + statement: Transaction.(tester: JsonTestsData.JsonTable, user1: User, data1: DataHolder, testDb: TestDB) -> Unit +) { + val tester = JsonTestsData.JsonTable + + withDb(excludeSettings = exclude) { testDb -> + excludingH2Version1(testDb) { + SchemaUtils.create(tester) + + val user1 = User("Admin", null) + val data1 = DataHolder(user1, 10, true, null) + + tester.insert { it[jsonColumn] = data1 } + + statement(tester, user1, data1, testDb) + + SchemaUtils.drop(tester) + } + } +} + +fun DatabaseTestsBase.withJsonBTable( + exclude: List = emptyList(), + statement: Transaction.(tester: JsonTestsData.JsonBTable, user1: User, data1: DataHolder, testDb: TestDB) -> Unit +) { + val tester = JsonTestsData.JsonBTable + + withDb(excludeSettings = exclude) { testDb -> + excludingH2Version1(testDb) { + SchemaUtils.create(tester) + + val user1 = User("Admin", null) + val data1 = DataHolder(user1, 10, true, null) + + tester.insert { it[jsonBColumn] = data1 } + + statement(tester, user1, data1, testDb) + + SchemaUtils.drop(tester) + } + } +} + +fun DatabaseTestsBase.withJsonArrays( + exclude: List = emptyList(), + statement: Transaction.( + tester: JsonTestsData.JsonArrays, + singleId: EntityID, + tripleId: EntityID, + testDb: TestDB + ) -> Unit +) { + val tester = JsonTestsData.JsonArrays + + withDb(excludeSettings = exclude) { testDb -> + excludingH2Version1(testDb) { + SchemaUtils.create(tester) + + val singleId = tester.insertAndGetId { + it[tester.groups] = UserGroup(listOf(User("A", "Team A"))) + it[tester.numbers] = intArrayOf(100) + } + val tripleId = tester.insertAndGetId { + it[tester.groups] = UserGroup(List(3) { i -> User("${'B' + i}", "Team ${'B' + i}") }) + it[tester.numbers] = intArrayOf(3, 4, 5) + } + + statement(tester, singleId, tripleId, testDb) + + SchemaUtils.drop(tester) + } + } +} + +fun DatabaseTestsBase.withJsonBArrays( + exclude: List = emptyList(), + statement: Transaction.( + tester: JsonTestsData.JsonBArrays, + singleId: EntityID, + tripleId: EntityID, + testDb: TestDB + ) -> Unit +) { + val tester = JsonTestsData.JsonBArrays + + withDb(excludeSettings = exclude) { testDb -> + excludingH2Version1(testDb) { + SchemaUtils.create(tester) + + val singleId = tester.insertAndGetId { + it[tester.groups] = UserGroup(listOf(User("A", "Team A"))) + it[tester.numbers] = intArrayOf(100) + } + val tripleId = tester.insertAndGetId { + it[tester.groups] = UserGroup(List(3) { i -> User("${'B' + i}", "Team ${'B' + i}") }) + it[tester.numbers] = intArrayOf(3, 4, 5) + } + + statement(tester, singleId, tripleId, testDb) + + SchemaUtils.drop(tester) + } + } +} + +@Serializable +data class DataHolder(val user: User, val logins: Int, val active: Boolean, val team: String?) + +@Serializable +data class User(val name: String, val team: String?) + +@Serializable +data class UserGroup(val users: List) diff --git a/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api b/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api new file mode 100644 index 0000000000..c04f7e1aab --- /dev/null +++ b/exposed-kotlin-datetime/api/exposed-kotlin-datetime.api @@ -0,0 +1,180 @@ +public final class org/jetbrains/exposed/sql/kotlin/datetime/CurrentDate : org/jetbrains/exposed/sql/Function { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/kotlin/datetime/CurrentDate; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/CurrentDateTime : org/jetbrains/exposed/sql/Function { + public static final field INSTANCE Lorg/jetbrains/exposed/sql/kotlin/datetime/CurrentDateTime; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/CurrentTimestamp : org/jetbrains/exposed/sql/Function { + public fun ()V + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnTypeKt { + public static final fun date (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun datetime (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun duration (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun time (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun timestamp (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public static final fun timestampWithTimeZone (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctionsKt { + public static final fun CustomDateFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomDateTimeFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomDurationFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimeFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimeStampFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun CustomTimestampWithTimeZoneFunction (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/CustomFunction; + public static final fun InstantDateExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantDateFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantDayExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantDayFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantHourExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantHourFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantMinuteExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantMinuteFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantMonthExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantMonthFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantSecondExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantSecondFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantYearExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun InstantYearFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateDateExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateDateFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateDayExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateDayFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateHourExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateHourFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateMinuteExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateMinuteFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateMonthExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateMonthFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateSecondExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateSecondFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeDateExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeDateFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeDayExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeDayFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeHourExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeHourFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeMinuteExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeMinuteFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeMonthExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeMonthFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeSecondExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeSecondFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeTimeFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeYearExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateTimeYearFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateYearExt (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun LocalDateYearFunction (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Function; + public static final fun dateLiteral (Lkotlinx/datetime/LocalDate;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun dateParam (Lkotlinx/datetime/LocalDate;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun dateTimeLiteral (Lkotlinx/datetime/LocalDateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun dateTimeParam (Lkotlinx/datetime/LocalDateTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun durationLiteral-LRDsOJo (J)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun durationParam-LRDsOJo (J)Lorg/jetbrains/exposed/sql/Expression; + public static final fun timeLiteral (Lkotlinx/datetime/LocalTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timeParam (Lkotlinx/datetime/LocalTime;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun timestampLiteral (Lkotlinx/datetime/Instant;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timestampParam (Lkotlinx/datetime/Instant;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun timestampWithTimeZoneLiteral (Ljava/time/OffsetDateTime;)Lorg/jetbrains/exposed/sql/LiteralOp; + public static final fun timestampWithTimeZoneParam (Ljava/time/OffsetDateTime;)Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDurationColumnType : org/jetbrains/exposed/sql/ColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/kotlin/datetime/KotlinDurationColumnType$Companion; + public fun ()V + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB-5sfh64U (Ljava/lang/Object;)J +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinDurationColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinInstantColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/kotlin/datetime/KotlinInstantColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Lkotlinx/datetime/Instant; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinInstantColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalDateColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalDateColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalDateColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalDateTimeColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalDateTimeColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalDateTimeColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalTimeColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalTimeColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Lkotlinx/datetime/LocalTime; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinLocalTimeColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinOffsetDateTimeColumnType : org/jetbrains/exposed/sql/ColumnType, org/jetbrains/exposed/sql/IDateColumnType { + public static final field Companion Lorg/jetbrains/exposed/sql/kotlin/datetime/KotlinOffsetDateTimeColumnType$Companion; + public fun ()V + public fun getHasTimePart ()Z + public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun readObject (Ljava/sql/ResultSet;I)Ljava/lang/Object; + public fun sqlType ()Ljava/lang/String; + public synthetic fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/time/OffsetDateTime; +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/KotlinOffsetDateTimeColumnType$Companion { +} + +public final class org/jetbrains/exposed/sql/kotlin/datetime/YearInternal : org/jetbrains/exposed/sql/Function { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; + public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V +} + diff --git a/exposed-kotlin-datetime/build.gradle.kts b/exposed-kotlin-datetime/build.gradle.kts index b477a38d0c..b7992f85f3 100644 --- a/exposed-kotlin-datetime/build.gradle.kts +++ b/exposed-kotlin-datetime/build.gradle.kts @@ -3,25 +3,31 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { kotlin("jvm") apply true - id("testWithDBs") + kotlin("plugin.serialization") apply true } repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) api("org.jetbrains.kotlinx", "kotlinx-datetime-jvm", "0.4.0") testImplementation(project(":exposed-dao")) testImplementation(project(":exposed-tests")) + testImplementation(project(":exposed-json")) testImplementation("junit", "junit", "4.12") testImplementation(kotlin("test-junit")) } 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-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt index 0837c105d0..a4d9432966 100644 --- a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt +++ b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateColumnType.kt @@ -1,5 +1,3 @@ -@file:Suppress("PrivatePropertyName") - package org.jetbrains.exposed.sql.kotlin.datetime import kotlinx.datetime.* @@ -9,11 +7,13 @@ import org.jetbrains.exposed.sql.ColumnType import org.jetbrains.exposed.sql.IDateColumnType import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.OracleDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect import org.jetbrains.exposed.sql.vendors.currentDialect import org.jetbrains.exposed.sql.vendors.h2Mode import java.sql.ResultSet +import java.time.OffsetDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.* @@ -50,6 +50,34 @@ private val DEFAULT_TIME_STRING_FORMATTER by lazy { DateTimeFormatter.ISO_LOCAL_TIME.withLocale(Locale.ROOT).withZone(ZoneId.systemDefault()) } +// Example result: 2023-07-07 14:42:29.343+02:00 or 2023-07-07 12:42:29.343Z +internal val SQLITE_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSS[XXX]", + Locale.ROOT + ) +} + +// For UTC time zone, MySQL rejects the 'Z' and will only accept the offset '+00:00' +internal val MYSQL_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSSSSS[xxx]", + Locale.ROOT + ) +} + +// Example result: 2023-07-07 14:42:29.343789 +02:00 +internal val ORACLE_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss.SSSSSS [xxx]", + Locale.ROOT + ) +} + +internal val DEFAULT_OFFSET_DATE_TIME_FORMATTER by lazy { + DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(Locale.ROOT) +} + private fun formatterForDateString(date: String) = dateTimeWithFractionFormat(date.substringAfterLast('.', "").length) private fun dateTimeWithFractionFormat(fraction: Int): DateTimeFormatter { val baseFormat = "yyyy-MM-d HH:mm:ss" @@ -251,6 +279,56 @@ class KotlinInstantColumnType : ColumnType(), IDateColumnType { } } +class KotlinOffsetDateTimeColumnType : ColumnType(), IDateColumnType { + override val hasTimePart: Boolean = true + + override fun sqlType(): String = currentDialect.dataTypeProvider.timestampWithTimeZoneType() + + override fun nonNullValueToString(value: Any): String = when (value) { + is OffsetDateTime -> { + when (currentDialect) { + is SQLiteDialect -> "'${value.format(SQLITE_OFFSET_DATE_TIME_FORMATTER)}'" + is MysqlDialect -> "'${value.format(MYSQL_OFFSET_DATE_TIME_FORMATTER)}'" + is OracleDialect -> "'${value.format(ORACLE_OFFSET_DATE_TIME_FORMATTER)}'" + else -> "'${value.format(DEFAULT_OFFSET_DATE_TIME_FORMATTER)}'" + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + override fun valueFromDB(value: Any): OffsetDateTime = when (value) { + is OffsetDateTime -> value + is String -> { + if (currentDialect is SQLiteDialect) { + OffsetDateTime.parse(value, SQLITE_OFFSET_DATE_TIME_FORMATTER) + } else { + OffsetDateTime.parse(value) + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + override fun readObject(rs: ResultSet, index: Int): Any? = when (currentDialect) { + is SQLiteDialect -> super.readObject(rs, index) + else -> rs.getObject(index, OffsetDateTime::class.java) + } + + override fun notNullValueToDB(value: Any): Any = when (value) { + is OffsetDateTime -> { + when (currentDialect) { + is SQLiteDialect -> value.format(SQLITE_OFFSET_DATE_TIME_FORMATTER) + is MysqlDialect -> value.format(MYSQL_OFFSET_DATE_TIME_FORMATTER) + else -> value + } + } + else -> error("Unexpected value: $value of ${value::class.qualifiedName}") + } + + companion object { + internal val INSTANCE = KotlinOffsetDateTimeColumnType() + } +} + class KotlinDurationColumnType : ColumnType() { override fun sqlType(): String = currentDialect.dataTypeProvider.longType() @@ -298,7 +376,7 @@ class KotlinDurationColumnType : ColumnType() { fun Table.date(name: String): Column = registerColumn(name, KotlinLocalDateColumnType()) /** - * A datetime column to store both a date and a time. + * A datetime column to store both a date and a time without time zone. * * @param name The column name */ @@ -314,12 +392,23 @@ fun Table.datetime(name: String): Column = registerColumn(name, K fun Table.time(name: String): Column = registerColumn(name, KotlinLocalTimeColumnType()) /** - * A timestamp column to store both a date and a time. + * A timestamp column to store both a date and a time without time zone. * * @param name The column name */ fun Table.timestamp(name: String): Column = registerColumn(name, KotlinInstantColumnType()) +/** + * A timestamp column to store both a date and a time with time zone. + * + * Note: PostgreSQL and MySQL always store the timestamp in UTC, thereby losing the original time zone. To preserve the + * original time zone, store the time zone information in a separate column. + * + * @param name The column name + */ +fun Table.timestampWithTimeZone(name: String): Column = + registerColumn(name, KotlinOffsetDateTimeColumnType()) + /** * A date column to store a duration. * diff --git a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt index 4d502f5db9..7aeca35106 100644 --- a/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt +++ b/exposed-kotlin-datetime/src/main/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinDateFunctions.kt @@ -13,8 +13,8 @@ import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.SQLServerDialect import org.jetbrains.exposed.sql.vendors.currentDialect import org.jetbrains.exposed.sql.vendors.h2Mode +import java.time.OffsetDateTime import kotlin.time.Duration -import kotlin.time.ExperimentalTime internal class DateInternal(val expr: Expression<*>) : Function(KotlinLocalDateColumnType.INSTANCE) { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("DATE(", expr, ")") } @@ -22,8 +22,10 @@ internal class DateInternal(val expr: Expression<*>) : Function(Kotli @JvmName("LocalDateDateFunction") fun Date(expr: Expression): Function = DateInternal(expr) + @JvmName("LocalDateTimeDateFunction") fun Date(expr: Expression): Function = DateInternal(expr) + @JvmName("InstantDateFunction") fun Date(expr: Expression): Function = DateInternal(expr) @@ -33,8 +35,10 @@ internal class TimeFunction(val expr: Expression<*>) : Function(Kotli @JvmName("LocalDateTimeFunction") fun Time(expr: Expression): Function = TimeFunction(expr) + @JvmName("LocalDateTimeTimeFunction") fun Time(expr: Expression): Function = TimeFunction(expr) + @JvmName("InstantTimeFunction") fun Time(expr: Expression): Function = TimeFunction(expr) @@ -45,14 +49,6 @@ object CurrentDateTime : Function(KotlinLocalDateTimeColumnType.I else -> "CURRENT_TIMESTAMP" } } - - @Deprecated( - message = "This class is now a singleton, no need for its constructor call; " + - "this method is provided for backward-compatibility only, and will be removed in future releases", - replaceWith = ReplaceWith("this"), - level = DeprecationLevel.ERROR, - ) - operator fun invoke() = this } object CurrentDate : Function(KotlinLocalDateColumnType.INSTANCE) { @@ -65,7 +61,7 @@ object CurrentDate : Function(KotlinLocalDateColumnType.INSTANCE) { } } -class CurrentTimestamp : Expression() { +class CurrentTimestamp : Function(KotlinInstantColumnType.INSTANCE) { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { +when { (currentDialect as? MysqlDialect)?.isFractionDateTimeSupported() == true -> "CURRENT_TIMESTAMP(6)" @@ -87,8 +83,10 @@ class YearInternal(val expr: Expression<*>) : Function(IntegerColumnType()) @JvmName("LocalDateYearFunction") fun Year(expr: Expression): Function = YearInternal(expr) + @JvmName("LocalDateTimeYearFunction") fun Year(expr: Expression): Function = YearInternal(expr) + @JvmName("InstantYearFunction") fun Year(expr: Expression): Function = YearInternal(expr) @@ -105,8 +103,10 @@ internal class MonthInternal(val expr: Expression<*>) : Function(IntegerCol @JvmName("LocalDateMonthFunction") fun Month(expr: Expression): Function = MonthInternal(expr) + @JvmName("LocalDateTimeMonthFunction") fun Month(expr: Expression): Function = MonthInternal(expr) + @JvmName("InstantMonthFunction") fun Month(expr: Expression): Function = MonthInternal(expr) @@ -123,8 +123,10 @@ internal class DayInternal(val expr: Expression<*>) : Function(IntegerColum @JvmName("LocalDateDayFunction") fun Day(expr: Expression): Function = DayInternal(expr) + @JvmName("LocalDateTimeDayFunction") fun Day(expr: Expression): Function = DayInternal(expr) + @JvmName("InstantDayFunction") fun Day(expr: Expression): Function = DayInternal(expr) @@ -141,8 +143,10 @@ internal class HourInternal(val expr: Expression<*>) : Function(IntegerColu @JvmName("LocalDateHourFunction") fun Hour(expr: Expression): Function = HourInternal(expr) + @JvmName("LocalDateTimeHourFunction") fun Hour(expr: Expression): Function = HourInternal(expr) + @JvmName("InstantHourFunction") fun Hour(expr: Expression): Function = HourInternal(expr) @@ -159,8 +163,10 @@ internal class MinuteInternal(val expr: Expression<*>) : Function(IntegerCo @JvmName("LocalDateMinuteFunction") fun Minute(expr: Expression): Function = MinuteInternal(expr) + @JvmName("LocalDateTimeMinuteFunction") fun Minute(expr: Expression): Function = MinuteInternal(expr) + @JvmName("InstantMinuteFunction") fun Minute(expr: Expression): Function = MinuteInternal(expr) @@ -177,8 +183,10 @@ internal class SecondInternal(val expr: Expression<*>) : Function(IntegerCo @JvmName("LocalDateSecondFunction") fun Second(expr: Expression): Function = SecondInternal(expr) + @JvmName("LocalDateTimeSecondFunction") fun Second(expr: Expression): Function = SecondInternal(expr) + @JvmName("InstantSecondFunction") fun Second(expr: Expression): Function = SecondInternal(expr) @@ -186,50 +194,64 @@ fun Second(expr: Expression): Function = SecondInternal(e @JvmName("LocalDateDateExt") fun Expression.date() = Date(this) + @JvmName("LocalDateTimeDateExt") fun Expression.date() = Date(this) + @JvmName("InstantDateExt") fun Expression.date() = Date(this) @JvmName("LocalDateYearExt") fun Expression.year() = Year(this) + @JvmName("LocalDateTimeYearExt") fun Expression.year() = Year(this) + @JvmName("InstantYearExt") fun Expression.year() = Year(this) @JvmName("LocalDateMonthExt") fun Expression.month() = Month(this) + @JvmName("LocalDateTimeMonthExt") fun Expression.month() = Month(this) + @JvmName("InstantMonthExt") fun Expression.month() = Month(this) @JvmName("LocalDateDayExt") fun Expression.day() = Day(this) + @JvmName("LocalDateTimeDayExt") fun Expression.day() = Day(this) + @JvmName("InstantDayExt") fun Expression.day() = Day(this) @JvmName("LocalDateHourExt") fun Expression.hour() = Hour(this) + @JvmName("LocalDateTimeHourExt") fun Expression.hour() = Hour(this) + @JvmName("InstantHourExt") fun Expression.hour() = Hour(this) @JvmName("LocalDateMinuteExt") fun Expression.minute() = Minute(this) + @JvmName("LocalDateTimeMinuteExt") fun Expression.minute() = Minute(this) + @JvmName("InstantMinuteExt") fun Expression.minute() = Minute(this) @JvmName("LocalDateSecondExt") fun Expression.second() = Second(this) + @JvmName("LocalDateTimeSecondExt") fun Expression.second() = Second(this) + @JvmName("InstantSecondExt") fun Expression.second() = Second(this) @@ -237,6 +259,10 @@ fun dateParam(value: LocalDate): Expression = QueryParameter(value, K fun timeParam(value: LocalTime): Expression = QueryParameter(value, KotlinLocalTimeColumnType.INSTANCE) fun dateTimeParam(value: LocalDateTime): Expression = QueryParameter(value, KotlinLocalDateTimeColumnType.INSTANCE) fun timestampParam(value: Instant): Expression = QueryParameter(value, KotlinInstantColumnType.INSTANCE) + +fun timestampWithTimeZoneParam(value: OffsetDateTime): Expression = + QueryParameter(value, KotlinOffsetDateTimeColumnType.INSTANCE) + fun durationParam(value: Duration): Expression = QueryParameter(value, KotlinDurationColumnType.INSTANCE) fun dateLiteral(value: LocalDate): LiteralOp = LiteralOp(KotlinLocalDateColumnType.INSTANCE, value) @@ -244,6 +270,10 @@ fun timeLiteral(value: LocalTime): LiteralOp = LiteralOp(KotlinLocalT fun dateTimeLiteral(value: LocalDateTime): LiteralOp = LiteralOp(KotlinLocalDateTimeColumnType.INSTANCE, value) fun timestampLiteral(value: Instant): LiteralOp = LiteralOp(KotlinInstantColumnType.INSTANCE, value) + +fun timestampWithTimeZoneLiteral(value: OffsetDateTime): LiteralOp = + LiteralOp(KotlinOffsetDateTimeColumnType.INSTANCE, value) + fun durationLiteral(value: Duration): LiteralOp = LiteralOp(KotlinDurationColumnType.INSTANCE, value) fun CustomDateFunction(functionName: String, vararg params: Expression<*>): CustomFunction = @@ -258,5 +288,11 @@ fun CustomDateTimeFunction(functionName: String, vararg params: Expression<*>): fun CustomTimeStampFunction(functionName: String, vararg params: Expression<*>): CustomFunction = CustomFunction(functionName, KotlinInstantColumnType.INSTANCE, *params) +@Suppress("FunctionName") +fun CustomTimestampWithTimeZoneFunction( + functionName: String, + vararg params: Expression<*> +): CustomFunction = CustomFunction(functionName, KotlinOffsetDateTimeColumnType.INSTANCE, *params) + fun CustomDurationFunction(functionName: String, vararg params: Expression<*>): CustomFunction = CustomFunction(functionName, KotlinDurationColumnType.INSTANCE, *params) diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt index 10c0519a38..840dc7800f 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/DefaultsTest.kt @@ -13,11 +13,14 @@ import org.jetbrains.exposed.sql.statements.BatchDataInconsistentException import org.jetbrains.exposed.sql.statements.BatchInsertStatement import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.constraintNamePart import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.inProperCase +import org.jetbrains.exposed.sql.tests.shared.Category.defaultExpression import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.H2Dialect import org.jetbrains.exposed.sql.vendors.MysqlDialect @@ -25,7 +28,9 @@ import org.jetbrains.exposed.sql.vendors.OracleDialect import org.jetbrains.exposed.sql.vendors.SQLServerDialect import org.jetbrains.exposed.sql.vendors.h2Mode import org.junit.Test -import org.junit.runners.model.MultipleFailureException +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -212,7 +217,7 @@ class DefaultsTest : DatabaseTestsBase() { val tmConstValue = LocalTime(12, 0) val tLiteral = timeLiteral(tmConstValue) - val TestTable = object : IntIdTable("t") { + val testTable = object : IntIdTable("t") { val s = varchar("s", 100).default("test") val sn = varchar("sn", 100).default("testNullable").nullable() val l = long("l").default(42) @@ -236,7 +241,7 @@ class DefaultsTest : DatabaseTestsBase() { else -> "NULL" } - withTables(listOf(TestDB.SQLITE), TestTable) { + withTables(listOf(TestDB.SQLITE), testTable) { val dtType = currentDialectTest.dataTypeProvider.dateTimeType() val longType = currentDialectTest.dataTypeProvider.longType() val timeType = currentDialectTest.dataTypeProvider.timeType() @@ -245,20 +250,20 @@ class DefaultsTest : DatabaseTestsBase() { val baseExpression = "CREATE TABLE " + addIfNotExistsIfSupported() + "${"t".inProperCase()} (" + "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerAutoincType()} PRIMARY KEY, " + - "${"s".inProperCase()} $varCharType DEFAULT 'test' NOT NULL, " + - "${"sn".inProperCase()} $varCharType DEFAULT 'testNullable' NULL, " + - "${"l".inProperCase()} ${currentDialectTest.dataTypeProvider.longType()} DEFAULT 42 NOT NULL, " + - "$q${"c".inProperCase()}$q CHAR DEFAULT 'X' NOT NULL, " + - "${"t1".inProperCase()} $dtType ${currentDT.itOrNull()}, " + - "${"t2".inProperCase()} $dtType ${nowExpression.itOrNull()}, " + - "${"t3".inProperCase()} $dtType ${dtLiteral.itOrNull()}, " + - "${"t4".inProperCase()} DATE ${dLiteral.itOrNull()}, " + - "${"t5".inProperCase()} $dtType ${tsLiteral.itOrNull()}, " + - "${"t6".inProperCase()} $dtType ${tsLiteral.itOrNull()}, " + - "${"t7".inProperCase()} $longType ${durLiteral.itOrNull()}, " + - "${"t8".inProperCase()} $longType ${durLiteral.itOrNull()}, " + - "${"t9".inProperCase()} $timeType ${tLiteral.itOrNull()}, " + - "${"t10".inProperCase()} $timeType ${tLiteral.itOrNull()}" + + "${"s".inProperCase()} $varCharType${testTable.s.constraintNamePart()} DEFAULT 'test' NOT NULL, " + + "${"sn".inProperCase()} $varCharType${testTable.sn.constraintNamePart()} DEFAULT 'testNullable' NULL, " + + "${"l".inProperCase()} ${currentDialectTest.dataTypeProvider.longType()}${testTable.l.constraintNamePart()} DEFAULT 42 NOT NULL, " + + "$q${"c".inProperCase()}$q CHAR${testTable.c.constraintNamePart()} DEFAULT 'X' NOT NULL, " + + "${"t1".inProperCase()} $dtType${testTable.t1.constraintNamePart()} ${currentDT.itOrNull()}, " + + "${"t2".inProperCase()} $dtType${testTable.t2.constraintNamePart()} ${nowExpression.itOrNull()}, " + + "${"t3".inProperCase()} $dtType${testTable.t3.constraintNamePart()} ${dtLiteral.itOrNull()}, " + + "${"t4".inProperCase()} DATE${testTable.t4.constraintNamePart()} ${dLiteral.itOrNull()}, " + + "${"t5".inProperCase()} $dtType${testTable.t5.constraintNamePart()} ${tsLiteral.itOrNull()}, " + + "${"t6".inProperCase()} $dtType${testTable.t6.constraintNamePart()} ${tsLiteral.itOrNull()}, " + + "${"t7".inProperCase()} $longType${testTable.t7.constraintNamePart()} ${durLiteral.itOrNull()}, " + + "${"t8".inProperCase()} $longType${testTable.t8.constraintNamePart()} ${durLiteral.itOrNull()}, " + + "${"t9".inProperCase()} $timeType${testTable.t9.constraintNamePart()} ${tLiteral.itOrNull()}, " + + "${"t10".inProperCase()} $timeType${testTable.t10.constraintNamePart()} ${tLiteral.itOrNull()}" + ")" val expected = if (currentDialectTest is OracleDialect || currentDialectTest.h2Mode == H2Dialect.H2CompatibilityMode.Oracle) { @@ -267,29 +272,28 @@ class DefaultsTest : DatabaseTestsBase() { arrayListOf(baseExpression) } - assertEqualLists(expected, TestTable.ddl) - - val id1 = TestTable.insertAndGetId { } - - val row1 = TestTable.select { TestTable.id eq id1 }.single() - assertEquals("test", row1[TestTable.s]) - assertEquals("testNullable", row1[TestTable.sn]) - assertEquals(42, row1[TestTable.l]) - assertEquals('X', row1[TestTable.c]) - assertEquals(dateTimeConstValue, row1[TestTable.t3]) - assertEquals(dateConstValue, row1[TestTable.t4]) - assertEquals(tsConstValue, row1[TestTable.t5]) - assertEquals(tsConstValue, row1[TestTable.t6]) - assertEquals(durConstValue, row1[TestTable.t7]) - assertEquals(durConstValue, row1[TestTable.t8]) - assertEquals(tmConstValue, row1[TestTable.t9]) - assertEquals(tmConstValue, row1[TestTable.t10]) + assertEqualLists(expected, testTable.ddl) + + val id1 = testTable.insertAndGetId { } + + val row1 = testTable.select { testTable.id eq id1 }.single() + assertEquals("test", row1[testTable.s]) + assertEquals("testNullable", row1[testTable.sn]) + assertEquals(42, row1[testTable.l]) + assertEquals('X', row1[testTable.c]) + assertEquals(dateTimeConstValue, row1[testTable.t3]) + assertEquals(dateConstValue, row1[testTable.t4]) + assertEquals(tsConstValue, row1[testTable.t5]) + assertEquals(tsConstValue, row1[testTable.t6]) + assertEquals(durConstValue, row1[testTable.t7]) + assertEquals(durConstValue, row1[testTable.t8]) + assertEquals(tmConstValue, row1[testTable.t9]) + assertEquals(tmConstValue, row1[testTable.t10]) } } @Test fun testDefaultExpressions01() { - fun abs(value: Int) = object : ExpressionWithColumnType() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("ABS($value)") } @@ -371,7 +375,10 @@ class DefaultsTest : DatabaseTestsBase() { fun testConsistentSchemeWithFunctionAsDefaultExpression() { val foo = object : IntIdTable("foo") { val name = text("name") - val defaultDateTime = datetime("defaultDateTime").defaultExpression(CurrentDateTime) + val defaultDate = date("default_date").defaultExpression(CurrentDate) + val defaultDateTime1 = datetime("default_date_time_1").defaultExpression(CurrentDateTime) + val defaultDateTime2 = datetime("default_date_time_2").defaultExpression(CurrentTimestamp()) + val defaultTimeStamp = timestamp("default_time_stamp").defaultExpression(CurrentTimestamp()) } withDb { @@ -379,10 +386,75 @@ class DefaultsTest : DatabaseTestsBase() { SchemaUtils.create(foo) val actual = SchemaUtils.statementsRequiredToActualizeScheme(foo) - assertTrue(actual.isEmpty()) + + if (currentDialectTest is MysqlDialect) { + // MySQL and MariaDB do not support CURRENT_DATE as default + // so the column is created with a NULL marker, which correctly triggers 1 alter statement + val tableName = foo.nameInDatabaseCase() + val dateColumnName = foo.defaultDate.nameInDatabaseCase() + val alter = "ALTER TABLE $tableName MODIFY COLUMN $dateColumnName DATE NULL" + assertEquals(alter, actual.single()) + } else { + assertTrue(actual.isEmpty()) + } } finally { SchemaUtils.drop(foo) } } } + + @Test + fun testTimestampWithTimeZoneDefaults() { + // UTC time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC)) + assertEquals("UTC", ZoneId.systemDefault().id) + + val nowWithTimeZone = OffsetDateTime.now() + val timestampWithTimeZoneLiteral = timestampWithTimeZoneLiteral(nowWithTimeZone) + + val testTable = object : IntIdTable("t") { + val t1 = timestampWithTimeZone("t1").default(nowWithTimeZone) + val t2 = timestampWithTimeZone("t2").defaultExpression(timestampWithTimeZoneLiteral) + } + + fun Expression<*>.itOrNull() = when { + currentDialectTest.isAllowedAsColumnDefault(this) -> + "DEFAULT ${currentDialectTest.dataTypeProvider.processForDefaultValue(this)} NOT NULL" + else -> "NULL" + } + + withDb(excludeSettings = listOf(TestDB.SQLITE, TestDB.MARIADB)) { + if (!isOldMySql()) { + SchemaUtils.create(testTable) + + val timestampWithTimeZoneType = currentDialectTest.dataTypeProvider.timestampWithTimeZoneType() + + val baseExpression = "CREATE TABLE " + addIfNotExistsIfSupported() + + "${"t".inProperCase()} (" + + "${"id".inProperCase()} ${currentDialectTest.dataTypeProvider.integerAutoincType()} PRIMARY KEY, " + + "${"t1".inProperCase()} $timestampWithTimeZoneType${testTable.t1.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}, " + + "${"t2".inProperCase()} $timestampWithTimeZoneType${testTable.t2.constraintNamePart()} ${timestampWithTimeZoneLiteral.itOrNull()}" + + ")" + + 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 + ) + } else { + arrayListOf(baseExpression) + } + + assertEqualLists(expected, testTable.ddl) + + val id1 = testTable.insertAndGetId { } + + val row1 = testTable.select { testTable.id eq id1 }.single() + assertEqualDateTime(nowWithTimeZone, row1[testTable.t1]) + assertEqualDateTime(nowWithTimeZone, row1[testTable.t2]) + } + } + } } diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt index 92a779e4f8..872f558b5d 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/KotlinTimeTests.kt @@ -1,20 +1,29 @@ package org.jetbrains.exposed.sql.kotlin.datetime import kotlinx.datetime.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.between import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.like +import org.jetbrains.exposed.sql.json.extract +import org.jetbrains.exposed.sql.json.jsonb 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.tests.shared.assertTrue +import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.* import org.junit.Assert.fail import org.junit.Test import java.math.BigDecimal import java.math.RoundingMode +import java.time.OffsetDateTime +import java.time.ZoneId import java.time.ZoneOffset import kotlin.test.assertEquals @@ -49,21 +58,21 @@ open class KotlinTimeBaseTest : DatabaseTestsBase() { // Checks that old numeric datetime columns works fine with new text representation @Test fun testSQLiteDateTimeFieldRegression() { - val TestDate = object : IntIdTable("TestDate") { + val testDate = object : IntIdTable("TestDate") { val time = datetime("time").defaultExpression(CurrentDateTime) } withDb(TestDB.SQLITE) { try { exec("CREATE TABLE IF NOT EXISTS TestDate (id INTEGER PRIMARY KEY AUTOINCREMENT, \"time\" NUMERIC DEFAULT (CURRENT_TIMESTAMP) NOT NULL);") - TestDate.insert { } - val year = TestDate.time.year() - val month = TestDate.time.month() - val day = TestDate.time.day() - val hour = TestDate.time.hour() - val minute = TestDate.time.minute() + testDate.insert { } + val year = testDate.time.year() + val month = testDate.time.month() + val day = testDate.time.day() + val hour = testDate.time.hour() + val minute = testDate.time.minute() - val result = TestDate.slice(year, month, day, hour, minute).selectAll().single() + val result = testDate.slice(year, month, day, hour, minute).selectAll().single() val now = now() assertEquals(now.year, result[year]) @@ -72,56 +81,65 @@ open class KotlinTimeBaseTest : DatabaseTestsBase() { assertEquals(now.hour, result[hour]) assertEquals(now.minute, result[minute]) } finally { - SchemaUtils.drop(TestDate) + SchemaUtils.drop(testDate) } } } @Test - fun `test storing LocalDateTime with nanos`() { - val TestDate = object : IntIdTable("TestLocalDateTime") { + fun testStoringLocalDateTimeWithNanos() { + val testDate = object : IntIdTable("TestLocalDateTime") { val time = datetime("time") } - withTables(TestDate) { - val dateTimeWithNanos = Clock.System.now().plus(DateTimeUnit.NANOSECOND * 123).toLocalDateTime(TimeZone.currentSystemDefault()) - TestDate.insert { - it[TestDate.time] = dateTimeWithNanos + + withTables(testDate) { + val dateTime = Instant.parse("2023-05-04T05:04:00.000Z") // has 0 nanoseconds + val nanos = DateTimeUnit.NANOSECOND * 111111 + // insert 2 separate constants to ensure test's rounding mode matches DB precision + val dateTimeWithFewNanos = dateTime.plus(nanos).toLocalDateTime(TimeZone.currentSystemDefault()) + val dateTimeWithManyNanos = dateTime.plus(nanos * 7).toLocalDateTime(TimeZone.currentSystemDefault()) + testDate.insert { + it[testDate.time] = dateTimeWithFewNanos + } + testDate.insert { + it[testDate.time] = dateTimeWithManyNanos } - val dateTimeFromDB = TestDate.selectAll().single()[TestDate.time] - assertEqualDateTime(dateTimeWithNanos, dateTimeFromDB) + val dateTimesFromDB = testDate.selectAll().map { it[testDate.time] } + assertEqualDateTime(dateTimeWithFewNanos, dateTimesFromDB[0]) + assertEqualDateTime(dateTimeWithManyNanos, dateTimesFromDB[1]) } } @Test fun `test selecting Instant using expressions`() { - val TestTable = object : Table() { + val testTable = object : Table() { val ts = timestamp("ts") val tsn = timestamp("tsn").nullable() } val now = Clock.System.now() - withTables(TestTable) { - TestTable.insert { + withTables(testTable) { + testTable.insert { it[ts] = now it[tsn] = now } - val maxTsExpr = TestTable.ts.max() - val maxTimestamp = TestTable.slice(maxTsExpr).selectAll().single()[maxTsExpr] + val maxTsExpr = testTable.ts.max() + val maxTimestamp = testTable.slice(maxTsExpr).selectAll().single()[maxTsExpr] assertEqualDateTime(now, maxTimestamp) - val minTsExpr = TestTable.ts.min() - val minTimestamp = TestTable.slice(minTsExpr).selectAll().single()[minTsExpr] + val minTsExpr = testTable.ts.min() + val minTimestamp = testTable.slice(minTsExpr).selectAll().single()[minTsExpr] assertEqualDateTime(now, minTimestamp) - val maxTsnExpr = TestTable.tsn.max() - val maxNullableTimestamp = TestTable.slice(maxTsnExpr).selectAll().single()[maxTsnExpr] + val maxTsnExpr = testTable.tsn.max() + val maxNullableTimestamp = testTable.slice(maxTsnExpr).selectAll().single()[maxTsnExpr] assertEqualDateTime(now, maxNullableTimestamp) - val minTsnExpr = TestTable.tsn.min() - val minNullableTimestamp = TestTable.slice(minTsnExpr).selectAll().single()[minTsnExpr] + val minTsnExpr = testTable.tsn.min() + val minNullableTimestamp = testTable.slice(minTsnExpr).selectAll().single()[minTsnExpr] assertEqualDateTime(now, minNullableTimestamp) } } @@ -256,6 +274,181 @@ open class KotlinTimeBaseTest : DatabaseTestsBase() { assertEquals(2, createdIn2023.size) } } + + @Test + fun testLocalDateTimeComparison() { + val testTableDT = object : IntIdTable("test_table_dt") { + val created = datetime("created") + val modified = datetime("modified") + } + + withTables(testTableDT) { testDb -> + val mayTheFourth = "2011-05-04T13:00:21.871130789Z" + val mayTheFourthDT = Instant.parse(mayTheFourth).toLocalDateTime(TimeZone.currentSystemDefault()) + val nowDT = now() + val id1 = testTableDT.insertAndGetId { + it[created] = mayTheFourthDT + it[modified] = mayTheFourthDT + } + val id2 = testTableDT.insertAndGetId { + it[created] = mayTheFourthDT + it[modified] = nowDT + } + + // these DB take the nanosecond value 871_130_789 and round up to default precision (e.g. in Oracle: 871_131) + val requiresExplicitDTCast = listOf(TestDB.ORACLE, TestDB.H2_ORACLE, TestDB.H2_PSQL, TestDB.H2_SQLSERVER) + val dateTime = when (testDb) { + in requiresExplicitDTCast -> Cast(dateTimeParam(mayTheFourthDT), KotlinLocalDateTimeColumnType()) + else -> dateTimeParam(mayTheFourthDT) + } + val createdMayFourth = testTableDT.select { testTableDT.created eq dateTime }.count() + assertEquals(2, createdMayFourth) + + val modifiedAtSameDT = testTableDT.select { testTableDT.modified eq testTableDT.created }.single() + assertEquals(id1, modifiedAtSameDT[testTableDT.id]) + + val modifiedAtLaterDT = testTableDT.select { testTableDT.modified greater testTableDT.created }.single() + assertEquals(id2, modifiedAtLaterDT[testTableDT.id]) + } + } + + @Test + fun testDateTimeAsJsonB() { + val tester = object : Table("tester") { + val created = datetime("created") + val modified = jsonb("modified", Json.Default) + } + + withTables(excludeSettings = TestDB.allH2TestDB + TestDB.SQLITE + TestDB.SQLSERVER + TestDB.ORACLE, tester) { + val dateTimeNow = now() + tester.insert { + it[created] = dateTimeNow.date.minus(1, DateTimeUnit.YEAR).atTime(0, 0, 0) + it[modified] = ModifierData(1, dateTimeNow) + } + tester.insert { + it[created] = dateTimeNow.date.plus(1, DateTimeUnit.YEAR).atTime(0, 0, 0) + it[modified] = ModifierData(2, dateTimeNow) + } + + val prefix = if (currentDialectTest is PostgreSQLDialect) "" else "." + + // value extracted in same manner it is stored, a json string + val modifiedAsString = tester.modified.extract("${prefix}timestamp") + val allModifiedAsString = tester.slice(modifiedAsString).selectAll() + assertTrue(allModifiedAsString.all { it[modifiedAsString] == dateTimeNow.toString() }) + // value extracted as json, with implicit LocalDateTime serializer() performing conversions + val modifiedAsJson = tester.modified.extract("${prefix}timestamp", toScalar = false) + val allModifiedAsJson = tester.slice(modifiedAsJson).selectAll() + assertTrue(allModifiedAsJson.all { it[modifiedAsJson] == dateTimeNow }) + + // PostgreSQL requires explicit type cast to timestamp for in-DB comparison + val dateModified = if (currentDialectTest is PostgreSQLDialect) { + tester.modified.extract("${prefix}timestamp").castTo(KotlinLocalDateTimeColumnType()) + } else { + tester.modified.extract("${prefix}timestamp") + } + val modifiedBeforeCreation = tester.select { dateModified less tester.created }.single() + assertEquals(2, modifiedBeforeCreation[tester.modified].userId) + } + } + + @Test + fun testTimestampWithTimeZone() { + val testTable = object : IntIdTable("TestTable") { + val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column") + } + + withDb(excludeSettings = listOf(TestDB.MARIADB)) { testDB -> + if (!isOldMySql()) { + SchemaUtils.create(testTable) + + // Cairo time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Africa/Cairo")) + assertEquals("Africa/Cairo", ZoneId.systemDefault().id) + + val cairoNow = OffsetDateTime.now(ZoneId.systemDefault()) + + val cairoId = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInCairoTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + // UTC time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC)) + assertEquals("UTC", ZoneId.systemDefault().id) + + val cairoNowRetrievedInUTCTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + val utcID = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInUTCTimeZone = testTable.select { testTable.id eq utcID } + .single()[testTable.timestampWithTimeZone] + + // Tokyo time zone + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Asia/Tokyo")) + assertEquals("Asia/Tokyo", ZoneId.systemDefault().id) + + val cairoNowRetrievedInTokyoTimeZone = testTable.select { testTable.id eq cairoId } + .single()[testTable.timestampWithTimeZone] + + val tokyoID = testTable.insertAndGetId { + it[timestampWithTimeZone] = cairoNow + } + + val cairoNowInsertedInTokyoTimeZone = testTable.select { testTable.id eq tokyoID } + .single()[testTable.timestampWithTimeZone] + + // PostgreSQL and MySQL always store the timestamp in UTC, thereby losing the original time zone. + // To preserve the original time zone, store the time zone information in a separate column. + val isOriginalTimeZonePreserved = testDB !in listOf( + TestDB.POSTGRESQL, + TestDB.POSTGRESQLNG, + TestDB.MYSQL + ) + if (isOriginalTimeZonePreserved) { + // Assert that time zone is preserved when the same value is inserted in different time zones + assertEqualDateTime(cairoNow, cairoNowInsertedInCairoTimeZone) + assertEqualDateTime(cairoNow, cairoNowInsertedInUTCTimeZone) + assertEqualDateTime(cairoNow, cairoNowInsertedInTokyoTimeZone) + + // Assert that time zone is preserved when the same record is retrieved in different time zones + assertEqualDateTime(cairoNow, cairoNowRetrievedInUTCTimeZone) + assertEqualDateTime(cairoNow, cairoNowRetrievedInTokyoTimeZone) + } else { + // Assert equivalence in UTC when the same value is inserted in different time zones + assertEqualDateTime(cairoNowInsertedInCairoTimeZone, cairoNowInsertedInUTCTimeZone) + assertEqualDateTime(cairoNowInsertedInUTCTimeZone, cairoNowInsertedInTokyoTimeZone) + + // Assert equivalence in UTC when the same record is retrieved in different time zones + assertEqualDateTime(cairoNowRetrievedInUTCTimeZone, cairoNowRetrievedInTokyoTimeZone) + } + + // Reset to original time zone as set up in DatabaseTestsBase init block + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(ZoneOffset.UTC)) + assertEquals("UTC", ZoneId.systemDefault().id) + } + } + } + + @Test + fun testTimestampWithTimeZoneThrowsExceptionForUnsupportedDialects() { + val testTable = object : IntIdTable("TestTable") { + val timestampWithTimeZone = timestampWithTimeZone("timestamptz-column") + } + + withDb(db = listOf(TestDB.MYSQL, TestDB.MARIADB)) { testDB -> + if (testDB == TestDB.MARIADB || isOldMySql()) { + expectException { + SchemaUtils.create(testTable) + } + } + } + } } fun assertEqualDateTime(d1: T?, d2: T?) { @@ -269,7 +462,6 @@ fun assertEqualDateTime(d1: T?, d2: T?) { assertEqualFractionalPart(d1.nanosecond, d2.nanosecond) } } - d1 is LocalDateTime && d2 is LocalDateTime -> { assertEquals( d1.toJavaLocalDateTime().toEpochSecond(ZoneOffset.UTC), @@ -278,35 +470,42 @@ fun assertEqualDateTime(d1: T?, d2: T?) { ) assertEqualFractionalPart(d1.nanosecond, d2.nanosecond) } - d1 is Instant && d2 is Instant -> { assertEquals(d1.epochSeconds, d2.epochSeconds, "Failed on epoch seconds ${currentDialectTest.name}") assertEqualFractionalPart(d1.nanosecondsOfSecond, d2.nanosecondsOfSecond) } - + d1 is OffsetDateTime && d2 is OffsetDateTime -> { + assertEqualDateTime(d1.toLocalDateTime().toKotlinLocalDateTime(), d2.toLocalDateTime().toKotlinLocalDateTime()) + assertEquals(d1.offset, d2.offset) + } else -> assertEquals(d1, d2, "Failed on ${currentDialectTest.name}") } } private fun assertEqualFractionalPart(nano1: Int, nano2: Int) { - when (currentDialectTest) { - // nanoseconds (H2, Oracle & Sqlite could be here) - // assertEquals(nano1, nano2, "Failed on nano ${currentDialectTest.name}") + val dialect = currentDialectTest + val db = dialect.name + when (dialect) { // accurate to 100 nanoseconds - is SQLServerDialect -> assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds ${currentDialectTest.name}") + is SQLServerDialect -> + assertEquals(roundTo100Nanos(nano1), roundTo100Nanos(nano2), "Failed on 1/10th microseconds $db") // microseconds - is H2Dialect, is MariaDBDialect, is PostgreSQLDialect, is PostgreSQLNGDialect -> assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") - is MysqlDialect -> - if ((currentDialectTest as? MysqlDialect)?.isFractionDateTimeSupported() == true) { - // this should be uncommented, but mysql has different microseconds between save & read -// assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds ${currentDialectTest.name}") - } else { - // don't compare fractional part + is MariaDBDialect -> + assertEquals(floorToMicro(nano1), floorToMicro(nano2), "Failed on microseconds $db") + is H2Dialect, is PostgreSQLDialect, is MysqlDialect -> { + when ((dialect as? MysqlDialect)?.isFractionDateTimeSupported()) { + null, true -> { + assertEquals(roundToMicro(nano1), roundToMicro(nano2), "Failed on microseconds $db") + } + else -> {} // don't compare fractional part } + } // milliseconds - is OracleDialect -> assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - is SQLiteDialect -> assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds ${currentDialectTest.name}") - else -> fail("Unknown dialect ${currentDialectTest.name}") + is OracleDialect -> + assertEquals(roundToMilli(nano1), roundToMilli(nano2), "Failed on milliseconds $db") + is SQLiteDialect -> + assertEquals(floorToMilli(nano1), floorToMilli(nano2), "Failed on milliseconds $db") + else -> fail("Unknown dialect $db") } } @@ -318,6 +517,8 @@ private fun roundToMicro(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000), RoundingMode.HALF_UP).toInt() } +private fun floorToMicro(nanos: Int): Int = nanos / 1_000 + private fun roundToMilli(nanos: Int): Int { return BigDecimal(nanos).divide(BigDecimal(1_000_000), RoundingMode.HALF_UP).toInt() } @@ -332,3 +533,6 @@ object CitiesTime : IntIdTable("CitiesTime") { val name = varchar("name", 50) // Column val local_time = datetime("local_time").nullable() // Column } + +@Serializable +data class ModifierData(val userId: Int, val timestamp: LocalDateTime) diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/MiscTableTest.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/MiscTableTest.kt index 3bc45e2df1..fe3f1da867 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/MiscTableTest.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/MiscTableTest.kt @@ -1,4 +1,5 @@ @file:OptIn(ExperimentalTime::class) +@file:Suppress("MaximumLineLength", "LongMethod") package org.jetbrains.exposed.sql.kotlin.datetime @@ -251,10 +252,14 @@ class MiscTableTest : DatabaseTestsBase() { } } + // these DB take the datetime nanosecond value and round up to default precision + // which causes flaky comparison failures if not cast to TIMESTAMP first + private val requiresExplicitDTCast = listOf(TestDB.ORACLE, TestDB.H2_ORACLE, TestDB.H2_PSQL, TestDB.H2_SQLSERVER) + @Test fun testSelect01() { val tbl = Misc - withTables(tbl) { + withTables(tbl) { testDb -> val date = today val dateTime = now() val time = dateTime.time @@ -435,8 +440,12 @@ class MiscTableTest : DatabaseTestsBase() { dblcn = null ) + val dtValue = when (testDb) { + in requiresExplicitDTCast -> Cast(dateTimeParam(dateTime), KotlinLocalDateTimeColumnType()) + else -> dateTimeParam(dateTime) + } tbl.checkRowFull( - tbl.select { tbl.dt.eq(dateTime) }.single(), + tbl.select { tbl.dt.eq(dtValue) }.single(), by = 13, byn = null, sm = -10, @@ -691,7 +700,7 @@ class MiscTableTest : DatabaseTestsBase() { @Test fun testSelect02() { val tbl = Misc - withTables(tbl) { + withTables(tbl) { testDb -> val date = today val dateTime = now() val time = dateTime.time @@ -857,8 +866,12 @@ class MiscTableTest : DatabaseTestsBase() { dblcn = 567.89 ) + val dtValue = when (testDb) { + in requiresExplicitDTCast -> Cast(dateTimeParam(dateTime), KotlinLocalDateTimeColumnType()) + else -> dateTimeParam(dateTime) + } tbl.checkRowFull( - tbl.select { tbl.dt.eq(dateTime) }.single(), + tbl.select { tbl.dt.eq(dtValue) }.single(), by = 13, byn = 13, sm = -10, @@ -1241,7 +1254,7 @@ class MiscTableTest : DatabaseTestsBase() { exec("INSERT IGNORE INTO `zerodatetimetable` (dt1,dt2,ts1,ts2) VALUES ('0000-00-00 00:00:00', '0000-00-00 00:00:00', '0000-00-00 00:00:00', '0000-00-00 00:00:00');") val row = ZeroDateTimeTable.selectAll().first() - for (c in listOf(ZeroDateTimeTable.dt1, ZeroDateTimeTable.dt2, ZeroDateTimeTable.ts1, ZeroDateTimeTable.ts2)) { + listOf(ZeroDateTimeTable.dt1, ZeroDateTimeTable.dt2, ZeroDateTimeTable.ts1, ZeroDateTimeTable.ts2).forEach { c -> val actual = row[c] assertNull(actual, "$c expected null but was $actual") } diff --git a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/sqlserver/SQLServerDefaultsTest.kt b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/sqlserver/SQLServerDefaultsTest.kt index cf585d9f27..48cc103735 100644 --- a/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/sqlserver/SQLServerDefaultsTest.kt +++ b/exposed-kotlin-datetime/src/test/kotlin/org/jetbrains/exposed/sql/kotlin/datetime/sqlserver/SQLServerDefaultsTest.kt @@ -16,7 +16,6 @@ class SQLServerDefaultsTest : DatabaseTestsBase() { @Test fun testDefaultExpressionsForTemporalTable() { - fun databaseGeneratedTimestamp() = object : ExpressionWithColumnType() { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { +"DEFAULT" } override val columnType: IColumnType = KotlinLocalDateTimeColumnType() diff --git a/exposed-money/api/exposed-money.api b/exposed-money/api/exposed-money.api new file mode 100644 index 0000000000..9b2076a205 --- /dev/null +++ b/exposed-money/api/exposed-money.api @@ -0,0 +1,27 @@ +public final class org/jetbrains/exposed/sql/money/CompositeMoneyColumn : org/jetbrains/exposed/sql/BiCompositeColumn { + public fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)V + public final fun getAmount ()Lorg/jetbrains/exposed/sql/Column; + public final fun getCurrency ()Lorg/jetbrains/exposed/sql/Column; +} + +public final class org/jetbrains/exposed/sql/money/CompositeMoneyColumnKt { + public static final fun CompositeMoneyColumn (Lorg/jetbrains/exposed/sql/Table;IILjava/lang/String;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/money/CompositeMoneyColumn; +} + +public final class org/jetbrains/exposed/sql/money/CompositeMoneyColumnTypeKt { + public static final fun compositeMoney (Lorg/jetbrains/exposed/sql/Table;IILjava/lang/String;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/money/CompositeMoneyColumn; + public static final fun compositeMoney (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/money/CompositeMoneyColumn; + public static synthetic fun compositeMoney$default (Lorg/jetbrains/exposed/sql/Table;IILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/money/CompositeMoneyColumn; + public static final fun compositeMoneyNullable (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/money/CompositeMoneyColumn; +} + +public final class org/jetbrains/exposed/sql/money/CurrencyColumnType : org/jetbrains/exposed/sql/VarCharColumnType { + public fun ()V + public fun notNullValueToDB (Ljava/lang/Object;)Ljava/lang/Object; + public fun valueFromDB (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class org/jetbrains/exposed/sql/money/CurrencyColumnTypeKt { + public static final fun currency (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; +} + diff --git a/exposed-money/build.gradle.kts b/exposed-money/build.gradle.kts index abd56d702c..c2768d7154 100644 --- a/exposed-money/build.gradle.kts +++ b/exposed-money/build.gradle.kts @@ -1,12 +1,15 @@ plugins { kotlin("jvm") apply true - id("testWithDBs") } repositories { mavenCentral() } +kotlin { + jvmToolchain(8) +} + dependencies { api(project(":exposed-core")) api(project(":exposed-dao")) diff --git a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt index ccc7315533..73bcfb02c4 100644 --- a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt +++ b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt @@ -40,6 +40,7 @@ class CompositeMoneyColumn( amount = Column(table, amountName, DecimalColumnType(precision, scale)), diff --git a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt index 4e07231dec..343100bd2c 100644 --- a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt +++ b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt @@ -9,7 +9,10 @@ import javax.money.MonetaryAmount fun Table.compositeMoney(precision: Int, scale: Int, amountName: String, currencyName: String = amountName + "_C") = registerCompositeColumn(CompositeMoneyColumn(this, precision, scale, amountName, currencyName)) -fun Table.compositeMoney(amountColumn: Column, currencyColumn: Column): CompositeMoneyColumn { +fun Table.compositeMoney( + amountColumn: Column, + currencyColumn: Column +): CompositeMoneyColumn { return CompositeMoneyColumn(amountColumn, currencyColumn).also { if (amountColumn !in columns && currencyColumn !in columns) { registerCompositeColumn(it) @@ -18,7 +21,10 @@ fun Table.compositeMoney(amountColumn: Column, currencyColumn: Colum } @JvmName("compositeMoneyNullable") -fun Table.compositeMoney(amountColumn: Column, currencyColumn: Column) : CompositeMoneyColumn { +fun Table.compositeMoney( + amountColumn: Column, + currencyColumn: Column +): CompositeMoneyColumn { return CompositeMoneyColumn(amountColumn, currencyColumn).also { if (amountColumn !in columns && currencyColumn !in columns) { registerCompositeColumn(it) diff --git a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CurrencyColumnType.kt b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CurrencyColumnType.kt index 42f6dc1203..f986900bc5 100644 --- a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CurrencyColumnType.kt +++ b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CurrencyColumnType.kt @@ -11,6 +11,7 @@ import javax.money.Monetary * * @author Vladislav Kisel */ +@Suppress("MagicNumber") class CurrencyColumnType : VarCharColumnType(3) { override fun notNullValueToDB(value: Any): Any { diff --git a/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyTests.kt b/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyTests.kt index ebfec11473..443b7c4c04 100644 --- a/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyTests.kt +++ b/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyTests.kt @@ -80,7 +80,7 @@ open class MoneyBaseTest : DatabaseTestsBase() { @Test fun testNullableCompositeColumnInsertAndSelect() { - val table = object : IntIdTable("Table") { + val table = object : IntIdTable("CompositeTable") { val composite_money = compositeMoney(8, AMOUNT_SCALE, "composite_money").nullable() } diff --git a/exposed-spring-boot-starter/README.md b/exposed-spring-boot-starter/README.md index 1ca5b0da1a..13353c0b16 100644 --- a/exposed-spring-boot-starter/README.md +++ b/exposed-spring-boot-starter/README.md @@ -1,9 +1,9 @@ # Exposed Spring Boot Starter -This is a starter for [Spring Boot](https://spring.io/projects/spring-boot) to utilize [Exposed](https://github.com/JetBrains/Exposed) as the ORM instead of [Hibernate](https://hibernate.org/) +This is a starter for [Spring Boot](https://spring.io/projects/spring-boot) to utilize [Exposed](https://github.com/JetBrains/Exposed) as the ORM instead of [Hibernate](https://hibernate.org/). ## Getting Started -This starter will give you the latest version of [Exposed](https://github.com/JetBrains/Exposed) and the spring-transaction library along with [Spring Boot Data Starter JDBC](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jdbc) +This starter will give you the latest version of [Exposed](https://github.com/JetBrains/Exposed) and its `spring-transaction` library along with the [Spring Boot Starter Data JDBC](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jdbc). ### Maven ```mxml @@ -18,22 +18,37 @@ This starter will give you the latest version of [Exposed](https://github.com/Je org.jetbrains.exposed exposed-spring-boot-starter - 0.41.1 + 0.44.0 ``` -### Gradle +### Gradle Groovy ```groovy repositories { mavenCentral() } dependencies { - implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.41.1' + implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0' } ``` +### Gradle Kotlin DSL +In `build.gradle.kts`: +```kotlin +val exposedVersion: String by project +repositories { + mavenCentral() +} +dependencies { + implementation("org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion") +} +``` +In `gradle.properties` +```properties +exposedVersion=0.44.0 +``` ## Setting up a database connection -This starter utilizes spring-boot-starter-data-jdbc so all properties that you are used to for setting up a database in spring are applicable here. +This starter utilizes `spring-boot-starter-data-jdbc` so that all properties usually used for setting up a database in Spring are applicable here. ### application.properties (h2 example) ```properties @@ -43,28 +58,70 @@ spring.datasource.username=sa spring.datasource.password=password ``` -### Configuring Exposed -When using this starter, you can customize typical Exposed configuration by registering a [DatabaseConfig](https://github.com/JetBrains/Exposed/blob/master/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt). See the class itself for available configuration options. +## Configuring Exposed +When using this starter, you can customize the default Exposed configuration by registering a [DatabaseConfig](https://github.com/JetBrains/Exposed/blob/master/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt). See the class itself for available configuration options. Example: ```kotlin +import org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration +import org.jetbrains.exposed.sql.DatabaseConfig +import org.springframework.boot.autoconfigure.ImportAutoConfiguration +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + @Configuration +@ImportAutoConfiguration( + value = [ExposedAutoConfiguration::class], + exclude = [DataSourceTransactionManagerAutoConfiguration::class] +) class ExposedConfig { - @Bean - fun databaseConfig() = DatabaseConfig { - useNestedTransactions = true - } + @Bean + fun databaseConfig() = DatabaseConfig { + useNestedTransactions = true + } } ``` +In addition to applying the `ExposedAutoConfiguration` class, it is recommended that the `DataSourceTransactionManagerAutoConfiguration` class be excluded from auto-configuration. +This can be done as part of a custom configuration, as shown above. + +Alternatively, auto-configuration can be detailed directly on the Spring configuration class that is annotated using `@SpringBootApplication`: + +```kotlin +import org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration +import org.springframework.boot.autoconfigure.ImportAutoConfiguration +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +import org.springframework.boot.runApplication + +@SpringBootApplication +@ImportAutoConfiguration( + value = [ExposedAutoConfiguration::class], + exclude = [DataSourceTransactionManagerAutoConfiguration::class] +) +class MyApplication + +fun main(args: Array) { + runApplication(*args) +} +``` + +See the [official documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using.auto-configuration.disabling-specific) for more options to exclude auto-configuration classes. ## Automatic Schema Creation This starter will create the database schema if enabled automatically using any class that extends `org.jetbrains.exposed.sql.Table` -Sometimes you will want to exclude packages from that list, we have included the property `spring.exposed.excluded-packages` which will exclude everything under the provided package +Sometimes you will want to exclude packages from the list of auto-created schemas. In this event, the property `spring.exposed.excluded-packages` can be used to exclude everything under the provided packages. ### application.properties ```properties -spring.exposed.generate-ddl = true -spring.exposed.excluded-packages = com.example.models.ignore,com.example.utils +spring.exposed.generate-ddl=true +spring.exposed.excluded-packages=com.example.models.ignore,com.example.utils ``` + +## Sample + +Check out the [Exposed - Spring Boot sample](../samples/exposed-spring/README.md) for more details, for example: +- How to set up [a controller](../samples/exposed-spring/src/main/kotlin/controller/UserController.kt) to handle web requests +- How to implement [a service layer](../samples/exposed-spring/src/main/kotlin/service/UserService.kt) for database access logic diff --git a/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api b/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api new file mode 100644 index 0000000000..9efd8c47d5 --- /dev/null +++ b/exposed-spring-boot-starter/api/exposed-spring-boot-starter.api @@ -0,0 +1,22 @@ +public class org/jetbrains/exposed/spring/DatabaseInitializer : org/springframework/boot/ApplicationRunner, org/springframework/core/Ordered { + public static final field Companion Lorg/jetbrains/exposed/spring/DatabaseInitializer$Companion; + public static final field DATABASE_INITIALIZER_ORDER I + public fun (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V + public fun getOrder ()I + public fun run (Lorg/springframework/boot/ApplicationArguments;)V +} + +public final class org/jetbrains/exposed/spring/DatabaseInitializer$Companion { +} + +public final class org/jetbrains/exposed/spring/DatabaseInitializerKt { + public static final fun discoverExposedTables (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)Ljava/util/List; +} + +public class org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfiguration { + public fun (Lorg/springframework/context/ApplicationContext;)V + public fun databaseConfig ()Lorg/jetbrains/exposed/sql/DatabaseConfig; + public fun databaseInitializer ()Lorg/jetbrains/exposed/spring/DatabaseInitializer; + public fun springTransactionManager (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;)Lorg/jetbrains/exposed/spring/SpringTransactionManager; +} + diff --git a/exposed-spring-boot-starter/build.gradle.kts b/exposed-spring-boot-starter/build.gradle.kts index 7bb07f7e3c..a1708dcd48 100644 --- a/exposed-spring-boot-starter/build.gradle.kts +++ b/exposed-spring-boot-starter/build.gradle.kts @@ -1,6 +1,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.exposed.gradle.Versions +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") apply true @@ -10,6 +11,10 @@ repositories { mavenCentral() } +kotlin { + jvmToolchain(17) +} + dependencies { api(project(":exposed-core")) api(project(":exposed-dao")) @@ -19,13 +24,21 @@ dependencies { compileOnly("org.springframework.boot", "spring-boot-configuration-processor", Versions.springBoot) testImplementation("org.springframework.boot", "spring-boot-starter-test", Versions.springBoot) - testImplementation("org.springframework.boot", "spring-boot-starter-webflux", Versions.springBoot) // put in testImplementation so no hard dependency for those using the starter - testImplementation("com.h2database", "h2", Versions.h2) + // put in testImplementation so no hard dependency for those using the starter + testImplementation("org.springframework.boot", "spring-boot-starter-webflux", Versions.springBoot) + testImplementation("com.h2database", "h2", Versions.h2_v2) +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "17" + } } 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)) diff --git a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfiguration.kt b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfiguration.kt index 0ab921a78e..aecc90cef5 100644 --- a/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfiguration.kt +++ b/exposed-spring-boot-starter/src/main/kotlin/org/jetbrains/exposed/spring/autoconfigure/ExposedAutoConfiguration.kt @@ -4,18 +4,16 @@ import org.jetbrains.exposed.spring.DatabaseInitializer import org.jetbrains.exposed.spring.SpringTransactionManager import org.jetbrains.exposed.sql.DatabaseConfig import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.AutoConfigureAfter +import org.springframework.boot.autoconfigure.AutoConfiguration import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration import org.springframework.transaction.annotation.EnableTransactionManagement import javax.sql.DataSource -@Configuration -@AutoConfigureAfter(DataSourceAutoConfiguration::class) +@AutoConfiguration(after = [DataSourceAutoConfiguration::class]) @EnableTransactionManagement open class ExposedAutoConfiguration(private val applicationContext: ApplicationContext) { @@ -36,7 +34,7 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC @Bean @ConditionalOnMissingBean(DatabaseConfig::class) open fun databaseConfig(): DatabaseConfig { - return DatabaseConfig { } + return DatabaseConfig {} } @Bean diff --git a/exposed-spring-boot-starter/src/main/resources/META-INF/spring.factories b/exposed-spring-boot-starter/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 94646e77db..0000000000 --- a/exposed-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,4 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration -org.springframework.boot.autoconfigure.EnableAutoConfiguration.exclude=\ - org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration \ No newline at end of file diff --git a/exposed-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/exposed-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..38704b9d7c --- /dev/null +++ b/exposed-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/AuthorTable.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/AuthorTable.kt similarity index 90% rename from exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/AuthorTable.kt rename to exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/AuthorTable.kt index a7b2f98d7a..42963164b1 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/AuthorTable.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/AuthorTable.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageName", "InvalidPackageDeclaration") + package org.jetbrains.exposed.`jdbc-template` import org.jetbrains.exposed.dao.UUIDEntity diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/BookService.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/BookService.kt similarity index 96% rename from exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/BookService.kt rename to exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/BookService.kt index a675133620..fd06032497 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/BookService.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/BookService.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageName", "InvalidPackageDeclaration") + package org.jetbrains.exposed.`jdbc-template` import org.jetbrains.exposed.sql.transactions.transaction diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/JdbcConfiguration.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/JdbcConfiguration.kt similarity index 92% rename from exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/JdbcConfiguration.kt rename to exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/JdbcConfiguration.kt index 6757f85342..75675f676f 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/JdbcConfiguration.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/JdbcConfiguration.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageName", "InvalidPackageDeclaration") + package org.jetbrains.exposed.`jdbc-template` import org.jetbrains.exposed.spring.SpringTransactionManager diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/JdbcTemplateTests.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/JdbcTemplateTests.kt similarity index 96% rename from exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/JdbcTemplateTests.kt rename to exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/JdbcTemplateTests.kt index 9c2391179c..a49b47c5f0 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc-template/JdbcTemplateTests.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/jdbc_template/JdbcTemplateTests.kt @@ -1,3 +1,5 @@ +@file:Suppress("PackageName", "InvalidPackageDeclaration") + package org.jetbrains.exposed.`jdbc-template` import org.jetbrains.exposed.sql.SchemaUtils diff --git a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/Application.kt b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/Application.kt index 1161841bbb..24016d810f 100644 --- a/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/Application.kt +++ b/exposed-spring-boot-starter/src/test/kotlin/org/jetbrains/exposed/spring/Application.kt @@ -2,10 +2,11 @@ package org.jetbrains.exposed.spring import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration import org.springframework.scheduling.annotation.EnableAsync @EnableAsync -@SpringBootApplication +@SpringBootApplication(exclude = [DataSourceTransactionManagerAutoConfiguration::class]) open class Application { open fun main(args: Array) { 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 ff72ea5e07..d097f29409 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 @@ -1,5 +1,6 @@ package org.jetbrains.exposed.spring.autoconfigure +import org.jetbrains.exposed.spring.Application import org.jetbrains.exposed.spring.DatabaseInitializer import org.jetbrains.exposed.spring.SpringTransactionManager import org.jetbrains.exposed.spring.tables.TestTable @@ -9,9 +10,13 @@ import org.jetbrains.exposed.sql.selectAll import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.springframework.beans.factory.NoSuchBeanDefinitionException import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.context.annotation.Bean import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @@ -47,7 +52,22 @@ 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 + ) + } + + @Test + fun testClassExcludedFromAutoConfig() { + val contextRunner = ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(Application::class.java) + ) + contextRunner.run { context -> + assertThrows(NoSuchBeanDefinitionException::class.java) { + context.getBean(DataSourceTransactionManagerAutoConfiguration::class.java) + } + } } @TestConfiguration @@ -68,10 +88,12 @@ open class ExposedAutoConfigurationTest { @SpringBootTest( classes = [org.jetbrains.exposed.spring.Application::class], - properties = ["spring.datasource.url=jdbc:h2:mem:test", + properties = [ + "spring.datasource.url=jdbc:h2:mem:test", "spring.datasource.driver-class-name=org.h2.Driver", "spring.exposed.generate-ddl=true", - "spring.exposed.show-sql=true"] + "spring.exposed.show-sql=true" + ] ) open class ExposedAutoConfigurationTestAutoGenerateDDL { @@ -118,5 +140,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 8081974f1d..10afbed1ef 100644 --- a/exposed-tests/build.gradle.kts +++ b/exposed-tests/build.gradle.kts @@ -4,7 +4,10 @@ import org.jetbrains.exposed.gradle.Versions plugins { kotlin("jvm") apply true - id("testWithDBs") +} + +kotlin { + jvmToolchain(8) } repositories { @@ -13,30 +16,35 @@ 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("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", Versions.kotlinCoroutines) - implementation("org.testcontainers", "mysql", Versions.testContainers) - implementation("org.testcontainers", "postgresql", Versions.testContainers) + implementation("com.zaxxer", "HikariCP", "4.0.3") + testCompileOnly("mysql", "mysql-connector-java", Versions.mysql80) testCompileOnly("org.postgresql", "postgresql", Versions.postgre) testCompileOnly("com.impossibl.pgjdbc-ng", "pgjdbc-ng", Versions.postgreNG) compileOnly("com.h2database", "h2", Versions.h2) testCompileOnly("org.xerial", "sqlite-jdbc", Versions.sqlLite3) + testImplementation("io.github.hakky54:logcaptor:2.9.0") } 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 a3e078f18c..a5b659be77 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,175 +1,35 @@ 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 import org.jetbrains.exposed.sql.transactions.transaction 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, 1, db = tmp) { - 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() -@Suppress("UnnecessaryAbstractClass") +@RunWith(Parameterized::class) abstract class DatabaseTestsBase { init { TimeZone.setDefault(TimeZone.getTimeZone("UTC")) @@ -181,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() @@ -202,51 +76,55 @@ abstract class DatabaseTestsBase { } val database = dbSettings.db!! - try { - transaction(database.transactionManager.defaultIsolationLevel, 1, db = database) { - 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) + fun withTables(excludeSettings: Collection, vararg tables: Table, statement: Transaction.(TestDB) -> Unit) { + 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, 1, db = database) { - SchemaUtils.drop(*tables) - } } } } @@ -254,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 " @@ -286,12 +172,18 @@ abstract class DatabaseTestsBase { } fun Transaction.excludingH2Version1(dbSettings: TestDB, statement: Transaction.(TestDB) -> Unit) { - if (dbSettings !in TestDB.allH2TestDB || db.isVersionCovers(BigDecimal("2.0"))) { + if (dbSettings !in TestDB.allH2TestDB || (db.dialect as H2Dialect).isSecondVersion) { statement(dbSettings) } } - protected fun prepareSchemaForTest(schemaName: String) : Schema { - return Schema(schemaName, defaultTablespace = "USERS", temporaryTablespace = "TEMP ", quota = "20M", on = "USERS") - } + fun Transaction.isOldMySql(version: String = "8.0") = currentDialectTest is MysqlDialect && !db.isVersionCovers(BigDecimal(version)) + + 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/main/kotlin/org/jetbrains/exposed/sql/tests/TestUtils.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/TestUtils.kt index 1e113ef026..01171890c6 100644 --- a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/TestUtils.kt +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/TestUtils.kt @@ -1,7 +1,9 @@ package org.jetbrains.exposed.sql.tests +import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.DatabaseDialect +import org.jetbrains.exposed.sql.vendors.SQLServerDialect import java.util.EnumSet fun String.inProperCase(): String = TransactionManager.currentOrNull()?.db?.identifierManager?.inProperCase(this) ?: this @@ -15,3 +17,7 @@ val currentDialectIfAvailableTest: DatabaseDialect? get() = inline fun > enumSetOf(vararg elements: E): EnumSet = elements.toCollection(EnumSet.noneOf(E::class.java)) + +fun Column.constraintNamePart() = (currentDialectTest as? SQLServerDialect)?.let { + " CONSTRAINT DF_${table.tableName}_$name" +} ?: "" diff --git a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/shared/ForeignKeyTables.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/shared/ForeignKeyTables.kt new file mode 100644 index 0000000000..f44660754b --- /dev/null +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/shared/ForeignKeyTables.kt @@ -0,0 +1,27 @@ +package org.jetbrains.exposed.sql.tests.shared + +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.Table + +object Category : Table("Category") { + val id = integer("id") + val name = varchar(name = "name", length = 20) + + override val primaryKey = PrimaryKey(id) +} + +const val DEFAULT_CATEGORY_ID = 0 + +object Item : Table("Item") { + val id = integer("id") + val name = varchar(name = "name", length = 20) + val categoryId = integer("categoryId") + .default(DEFAULT_CATEGORY_ID) + .references( + Category.id, + onDelete = ReferenceOption.SET_DEFAULT, + onUpdate = ReferenceOption.NO_ACTION + ) + + override val primaryKey = PrimaryKey(id) +} 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 new file mode 100644 index 0000000000..c74af6b1dc --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/h2/ConnectionPoolTests.kt @@ -0,0 +1,71 @@ +package org.jetbrains.exposed.sql.tests.h2 + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +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 +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.Assume +import org.junit.Test + +class ConnectionPoolTests : LogDbInTestName() { + private val hikariDataSource1 by lazy { + HikariDataSource( + HikariConfig().apply { + jdbcUrl = "jdbc:h2:mem:hikariDB1" + maximumPoolSize = 10 + } + ) + } + + private val hikariDB1 by lazy { + Database.connect(hikariDataSource1) + } + + @Test + fun testSuspendTransactionsExceedingPoolSize() { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + transaction(db = hikariDB1) { + SchemaUtils.create(TestTable) + } + + val exceedsPoolSize = (hikariDataSource1.maximumPoolSize * 2 + 1).coerceAtMost(50) + runBlocking { + repeat(exceedsPoolSize) { + launch { + newSuspendedTransaction { + delay(100) + TestEntity.new { testValue = "test$it" } + } + } + } + } + + transaction(db = hikariDB1) { + assertEquals(exceedsPoolSize, TestEntity.all().toList().count()) + + SchemaUtils.drop(TestTable) + } + } + + object TestTable : IntIdTable("HIKARI_TESTER") { + val testValue = varchar("test_value", 32) + } + + class TestEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TestTable) + + var testValue by TestTable.testValue + } +} 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 df3d491124..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) @@ -190,7 +190,8 @@ class MultiDatabaseEntityTest { b2Reread.y = null } } - inTopLevelTransaction(Connection.TRANSACTION_READ_COMMITTED, 1, db = db1) { + inTopLevelTransaction(Connection.TRANSACTION_READ_COMMITTED, db = db1) { + repetitionAttempts = 1 assertNull(EntityTestsData.BEntity.testCache(db1b1.id)) val b1Reread = EntityTestsData.BEntity[db1b1.id] assertEquals(db1b1.id, b1Reread.id) 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 0b9ca30835..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 @@ -12,7 +12,7 @@ import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.tests.shared.dml.DMLTestsData import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransaction +import org.jetbrains.exposed.sql.transactions.experimental.withSuspendTransaction import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transactionManager import org.junit.After @@ -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 } @@ -192,7 +192,7 @@ class MultiDatabaseTest { assertEquals(2L, DMLTestsData.Cities.selectAll().count()) assertEquals("city3", DMLTestsData.Cities.selectAll().last()[DMLTestsData.Cities.name]) - tr1.suspendedTransaction { + tr1.withSuspendTransaction { assertEquals(1L, DMLTestsData.Cities.selectAll().count()) DMLTestsData.Cities.insert { it[DMLTestsData.Cities.name] = "city4" diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/mysql/MysqlTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/mysql/MysqlTests.kt index ba8cff7a4d..4e8d23a8cb 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/mysql/MysqlTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/mysql/MysqlTests.kt @@ -1,12 +1,18 @@ package org.jetbrains.exposed.sql.tests.mysql +import com.mysql.cj.conf.PropertyKey +import com.mysql.cj.jdbc.ConnectionImpl +import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.RepeatableTestRule import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.dml.DMLTestsData import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.Rule import org.junit.Test import kotlin.test.assertFalse +import kotlin.test.assertNotNull class MysqlTests : DatabaseTestsBase() { @@ -19,4 +25,22 @@ class MysqlTests : DatabaseTestsBase() { assertFalse(TransactionManager.current().exec("SELECT VERSION();") { it.next(); it.getString(1) }.isNullOrEmpty()) } } + + @Test + fun testBatchInsertWithRewriteBatchedStatementsOn() { + val mysqlOnly = TestDB.enabledDialects() - TestDB.MYSQL + withTables(excludeSettings = mysqlOnly, DMLTestsData.Cities) { + val mysqlConnection = this.connection.connection as ConnectionImpl + mysqlConnection.propertySet.getBooleanProperty(PropertyKey.rewriteBatchedStatements).value = true + val cityNames = listOf("FooCity", "BarCity") + val generatedValues = DMLTestsData.Cities.batchInsert(cityNames) { city -> + this[DMLTestsData.Cities.name] = city + } + + assertEquals(cityNames.size, generatedValues.size) + generatedValues.forEach { + assertNotNull(it.getOrNull(DMLTestsData.Cities.id)) + } + } + } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/postgresql/PostgresqlTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/postgresql/PostgresqlTests.kt index b78720fa69..5544351818 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/postgresql/PostgresqlTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/postgresql/PostgresqlTests.kt @@ -5,10 +5,14 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.RepeatableTestRule import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.shared.assertFailAndRollback +import org.jetbrains.exposed.sql.tests.shared.assertFalse +import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.vendors.ForUpdateOption import org.jetbrains.exposed.sql.vendors.ForUpdateOption.PostgreSQL import org.junit.Rule import org.junit.Test +import java.sql.ResultSet import kotlin.test.assertEquals class PostgresqlTests : DatabaseTestsBase() { @@ -61,6 +65,57 @@ class PostgresqlTests : DatabaseTestsBase() { } } + @Test + fun testPrimaryKeyCreatedInPostgresql() { + val tableName = "tester" + val tester1 = object : Table(tableName) { + val age = integer("age") + } + + val tester2 = object : Table(tableName) { + val age = integer("age") + + override val primaryKey = PrimaryKey(age) + } + + val tester3 = object : IntIdTable(tableName) { + val age = integer("age") + } + + fun Transaction.assertPrimaryKey(transform: (ResultSet) -> T): T? { + return exec( + """ + SELECT ct.relname as TABLE_NAME, ci.relname AS PK_NAME + FROM pg_catalog.pg_class ct + JOIN pg_index i ON (ct.oid = i.indrelid AND indisprimary) + JOIN pg_catalog.pg_class ci ON (ci.oid = i.indexrelid) + WHERE ct.relname IN ('$tableName') + """.trimIndent() + ) { rs -> + transform(rs) + } + } + withDb(listOf(TestDB.POSTGRESQLNG, TestDB.POSTGRESQL)) { + val defaultPKName = "tester_pkey" + SchemaUtils.createMissingTablesAndColumns(tester1) + assertPrimaryKey { + assertFalse(it.next()) + } + + SchemaUtils.createMissingTablesAndColumns(tester2) + assertPrimaryKey { + assertTrue(it.next()) + assertEquals(defaultPKName, it.getString("PK_NAME")) + } + + assertFailAndRollback("Multiple primary keys are not allowed") { + SchemaUtils.createMissingTablesAndColumns(tester3) + } + + SchemaUtils.drop(tester1) + } + } + private fun Transaction.withTable(statement: Transaction.() -> Unit) { SchemaUtils.create(table) try { 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/ConnectionTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionTests.kt index 7a4aee0a50..d28ed12b76 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ConnectionTests.kt @@ -45,7 +45,6 @@ class ConnectionTests : DatabaseTestsBase() { // GitHub issue #838 @Test - @Suppress("unused") fun testTableConstraints() { val parent = object : LongIdTable("parent") { val scale = integer("scale").uniqueIndex() 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/CoroutineTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/CoroutineTests.kt index 67bfafd1a9..d2f42f0ffb 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/CoroutineTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/CoroutineTests.kt @@ -13,7 +13,7 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.RepeatableTest import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransaction +import org.jetbrains.exposed.sql.transactions.experimental.withSuspendTransaction import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Rule @@ -31,6 +31,10 @@ class CoroutineTests : DatabaseTestsBase() { object Testing : IntIdTable("COROUTINE_TESTING") + object TestingUnique : Table("COROUTINE_UNIQUE") { + val id = integer("id").uniqueIndex() + } + @Rule @JvmField val timeout = CoroutinesTimeout.seconds(60) @@ -44,7 +48,7 @@ class CoroutineTests : DatabaseTestsBase() { newSuspendedTransaction(db = db) { Testing.insert {} - suspendedTransaction { + withSuspendTransaction { assertEquals(1, Testing.select { Testing.id.eq(1) }.singleOrNull()?.getOrNull(Testing.id)?.value) } } @@ -64,6 +68,50 @@ class CoroutineTests : DatabaseTestsBase() { } } + @Test @RepeatableTest(10) + fun testSuspendTransactionWithRepetition() { + withTables(TestingUnique) { + val (originalId, updatedId) = 1 to 99 + val mainJob = GlobalScope.async(Dispatchers.Default) { + newSuspendedTransaction(Dispatchers.Default, db = db) { + TestingUnique.insert { it[id] = originalId } + + assertEquals(originalId, TestingUnique.selectAll().single()[TestingUnique.id]) + } + + val insertJob = launch { + newSuspendedTransaction(Dispatchers.Default, db = db) { + repetitionAttempts = 20 + + // throws JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation + // until original row is updated with a new id + TestingUnique.insert { it[id] = originalId } + + assertEquals(2, TestingUnique.selectAll().count()) + } + } + val updateJob = launch { + newSuspendedTransaction(Dispatchers.Default, db = db) { + repetitionAttempts = 20 + + TestingUnique.update({ TestingUnique.id eq originalId }) { it[id] = updatedId } + + assertEquals(updatedId, TestingUnique.selectAll().single()[TestingUnique.id]) + } + } + insertJob.join() + updateJob.join() + + val result = newSuspendedTransaction(Dispatchers.Default, db = db) { TestingUnique.selectAll().count() } + kotlin.test.assertEquals(2, result, "Failing at end of mainJob") + } + + while (!mainJob.isCompleted) Thread.sleep(100) + mainJob.getCompletionExceptionOrNull()?.let { throw it } + assertEqualCollections(listOf(updatedId, originalId), TestingUnique.selectAll().map { it[TestingUnique.id] }) + } + } + @Test @RepeatableTest(10) fun suspendTxAsync() { withTables(Testing) { @@ -71,7 +119,7 @@ class CoroutineTests : DatabaseTestsBase() { val launchResult = suspendedTransactionAsync(Dispatchers.IO, db = db) { Testing.insert {} - suspendedTransaction { + withSuspendTransaction { assertEquals(1, Testing.select { Testing.id.eq(1) }.singleOrNull()?.getOrNull(Testing.id)?.value) } } @@ -96,6 +144,45 @@ class CoroutineTests : DatabaseTestsBase() { } } + @Test @RepeatableTest(10) + fun testSuspendTransactionAsyncWithRepetition() { + withTables(excludeSettings = listOf(TestDB.SQLITE), TestingUnique) { + val (originalId, updatedId) = 1 to 99 + val mainJob = GlobalScope.async(Dispatchers.Default) { + newSuspendedTransaction(Dispatchers.Default, db = db) { + TestingUnique.insert { it[id] = originalId } + + assertEquals(originalId, TestingUnique.selectAll().single()[TestingUnique.id]) + } + + val (insertResult, updateResult) = listOf( + suspendedTransactionAsync(db = db) { + repetitionAttempts = 20 + + // throws JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation + // until original row is updated with a new id + TestingUnique.insert { it[id] = originalId } + + TestingUnique.selectAll().count() + }, + suspendedTransactionAsync(db = db) { + repetitionAttempts = 20 + + TestingUnique.update({ TestingUnique.id eq originalId }) { it[id] = updatedId } + TestingUnique.selectAll().count() + } + ).awaitAll() + + kotlin.test.assertEquals(1, updateResult, "Failing at end of update job") + kotlin.test.assertEquals(2, insertResult, "Failing at end of insert job") + } + + while (!mainJob.isCompleted) Thread.sleep(100) + mainJob.getCompletionExceptionOrNull()?.let { throw it } + assertEqualCollections(listOf(updatedId, originalId), TestingUnique.selectAll().map { it[TestingUnique.id] }) + } + } + @Test @RepeatableTest(10) fun nestedSuspendTxTest() { suspend fun insertTesting(db: Database) = newSuspendedTransaction(db = db) { 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 603d536575..dd2e1a1136 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 @@ -1,5 +1,6 @@ package org.jetbrains.exposed.sql.tests.shared +import nl.altindag.log.LogCaptor import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID @@ -9,16 +10,22 @@ import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus import org.jetbrains.exposed.sql.statements.api.ExposedBlob 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.inProperCase import org.jetbrains.exposed.sql.tests.shared.dml.DMLTestsData +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.OracleDialect +import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect import org.jetbrains.exposed.sql.vendors.SQLServerDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect +import org.junit.Assume import org.junit.Test import org.postgresql.util.PGobject import java.util.* @@ -54,16 +61,151 @@ class DDLTests : DatabaseTestsBase() { } } - object KeyWordTable : IntIdTable(name = "keywords") { - val bool = bool("bool") + // TODO: Remove test when preserveKeywordCasing flag behavior becomes default + @Test + fun testKeywordIdentifiersWithoutOptIn() { + val keywords = listOf("Integer", "name") + val tester = object : Table(keywords[0]) { + val name = varchar(keywords[1], 32) + } + + withDb(excludeSettings = TestDB.allH2TestDB - TestDB.H2) { + assertFalse(db.config.preserveKeywordCasing) + + SchemaUtils.create(tester) + assertTrue(tester.exists()) + + val (tableName, columnName) = keywords.map { + when (currentDialectTest) { + is MysqlDialect -> "`$it`" + is PostgreSQLDialect -> "\"${it.lowercase()}\"" + is OracleDialect, is H2Dialect -> "\"${it.uppercase()}\"" + else -> "\"$it\"" + } + } + + val expectedCreate = "CREATE TABLE ${addIfNotExistsIfSupported()}$tableName (" + + "$columnName ${tester.name.columnType.sqlType()} NOT NULL)" + assertEquals(expectedCreate, tester.ddl.single()) + + // check that insert and select statement identifiers also match in DB without throwing SQLException + tester.insert { it[name] = "A" } + + val expectedSelect = "SELECT $tableName.$columnName FROM $tableName" + tester.selectAll().also { + assertEquals(expectedSelect, it.prepareSQL(this, prepared = false)) + } + + // check that identifiers match with returned jdbc metadata + val statements = SchemaUtils.statementsRequiredToActualizeScheme(tester) + assertTrue(statements.isEmpty()) + + SchemaUtils.drop(tester) + } } - @Test fun tableExistsWithKeyword() { - withTables(KeyWordTable) { - assertEquals(true, KeyWordTable.exists()) - KeyWordTable.insert { - it[bool] = true + private object FlagTestTable : Table("BooLean") { + val name = varchar("name", 32) + } + + // TODO: Remove test when preserveKeywordCasing flag behavior becomes default + @Test + fun testKeywordIdentifiersLogWarningWithoutOptIn() { + withDb { + val logCaptor = LogCaptor.forName(exposedLogger.name) + try { + SchemaUtils.create(FlagTestTable) + assertEquals(2, logCaptor.warnLogs.size) + logCaptor.clearLogs() + + FlagTestTable.insert { it[name] = "A" } + FlagTestTable.selectAll().toList() + SchemaUtils.drop(FlagTestTable) + assertEquals(0, logCaptor.warnLogs.size) + } finally { + logCaptor.clearLogs() + } + } + } + + private val keywordFlagDB by lazy { + Database.connect( + url = "jdbc:h2:mem:flagtest;DB_CLOSE_DELAY=-1;", + driver = "org.h2.Driver", + user = "root", + password = "", + databaseConfig = DatabaseConfig { + @OptIn(ExperimentalKeywordApi::class) + preserveKeywordCasing = true + } + ) + } + + // TODO: Refactor test to cover all DB when preserveKeywordCasing flag behavior becomes default + @Test + fun testKeywordIdentifiersWithOptIn() { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + + val keywords = listOf("data", "public", "key", "constraint") + val keywordTable = object : Table(keywords[0]) { + val public = bool(keywords[1]) + val data = integer(keywords[2]) + val constraint = varchar(keywords[3], 32) + } + + transaction(keywordFlagDB) { + assertTrue(db.config.preserveKeywordCasing) + + SchemaUtils.create(keywordTable) + assertTrue(keywordTable.exists()) + + val (tableName, publicName, dataName, constraintName) = keywords.map { "\"$it\"" } + + val expectedCreate = "CREATE TABLE ${addIfNotExistsIfSupported()}$tableName (" + + "$publicName ${keywordTable.public.columnType.sqlType()} NOT NULL, " + + "$dataName ${keywordTable.data.columnType.sqlType()} NOT NULL, " + + "$constraintName ${keywordTable.constraint.columnType.sqlType()} NOT NULL)" + assertEquals(expectedCreate, keywordTable.ddl.single()) + + // check that insert and select statement identifiers also match in DB without throwing SQLException + keywordTable.insert { + it[public] = false + it[data] = 999 + it[constraint] = "unique" } + + val expectedSelect = "SELECT $tableName.$publicName, $tableName.$dataName, $tableName.$constraintName FROM $tableName" + keywordTable.selectAll().also { + assertEquals(expectedSelect, it.prepareSQL(this, prepared = false)) + } + + // check that identifiers match with returned jdbc metadata + val statements = SchemaUtils.statementsRequiredToActualizeScheme(keywordTable) + assertTrue(statements.isEmpty()) + + SchemaUtils.drop(keywordTable) + } + TransactionManager.closeAndUnregister(keywordFlagDB) + } + + // TODO: Remove test when preserveKeywordCasing flag behavior becomes default + @Test + fun testKeywordIdentifiersLogNoWarningWithOptIn() { + Assume.assumeTrue(TestDB.H2 in TestDB.enabledDialects()) + + val logCaptor = LogCaptor.forName(exposedLogger.name) + try { + transaction(keywordFlagDB) { + SchemaUtils.create(FlagTestTable) + assertEquals(0, logCaptor.warnLogs.size) + logCaptor.clearLogs() + + SchemaUtils.drop(FlagTestTable) + assertEquals(0, logCaptor.warnLogs.size) + } + } finally { + logCaptor.clearLogs() + TransactionManager.closeAndUnregister(keywordFlagDB) } } @@ -395,7 +537,54 @@ class DDLTests : DatabaseTestsBase() { } } - @Test fun testBlob() { + @Test + fun testIndexWithFunctions() { + val tester = object : Table("tester") { + val amount = integer("amount") + val price = integer("price") + val item = varchar("item", 32).nullable() + + init { + index(customIndexName = "tester_plus_index", isUnique = false, functions = listOf(amount.plus(price))) + index(isUnique = false, functions = listOf(item.lowerCase())) + uniqueIndex(columns = arrayOf(price), functions = listOf(Coalesce(item, stringLiteral("*")))) + } + } + + withDb { testDb -> + val tableProperName = tester.tableName.inProperCase() + val priceColumnName = tester.price.nameInDatabaseCase() + val uniqueIndexName = "tester_price_coalesce${if (testDb == TestDB.SQLITE) "" else "_unique"}".inProperCase() + val (p1, p2) = if (testDb == TestDB.MYSQL) "(" to ")" else "" to "" + val functionStrings = when (testDb) { + TestDB.SQLITE, TestDB.ORACLE -> listOf("(amount + price)", "LOWER(item)", "COALESCE(item, '*')").map(String::inProperCase) + else -> listOf( + tester.amount.plus(tester.price).toString(), + "$p1${tester.item.lowerCase()}$p2", + "$p1${Coalesce(tester.item, stringLiteral("*"))}$p2" + ) + } + + val functionsNotSupported = testDb in (TestDB.allH2TestDB + TestDB.SQLSERVER + TestDB.MARIADB) || isOldMySql() + val expectedStatements = if (functionsNotSupported) { + List(3) { "" } + } else { + listOf( + "CREATE INDEX tester_plus_index ON $tableProperName (${functionStrings[0]})", + "CREATE INDEX ${"tester_lower".inProperCase()} ON $tableProperName (${functionStrings[1]})", + "CREATE UNIQUE INDEX $uniqueIndexName ON $tableProperName ($priceColumnName, ${functionStrings[2]})" + ) + } + + repeat(3) { i -> + val actualStatement = SchemaUtils.createIndex(tester.indices[i]) + assertEquals(expectedStatements[i], actualStatement) + } + } + } + + @Test + fun testBlob() { val t = object : Table("t1") { val id = integer("id").autoIncrement() val b = blob("blob") @@ -408,11 +597,6 @@ class DDLTests : DatabaseTestsBase() { val longBytes = Random.nextBytes(1024) val shortBlob = ExposedBlob(shortBytes) val longBlob = ExposedBlob(longBytes) -// if (currentDialectTest.dataTypeProvider.blobAsStream) { -// SerialBlob(bytes) -// } else connection.createBlob().apply { -// setBytes(1, bytes) -// } val id1 = t.insert { it[t.b] = shortBlob @@ -422,6 +606,10 @@ class DDLTests : DatabaseTestsBase() { it[t.b] = longBlob } get (t.id) + val id3 = t.insert { + it[t.b] = blobParam(ExposedBlob(shortBytes)) + } get (t.id) + val readOn1 = t.select { t.id eq id1 }.first()[t.b] val text1 = String(readOn1.bytes) val text2 = readOn1.inputStream.bufferedReader().readText() @@ -435,6 +623,9 @@ class DDLTests : DatabaseTestsBase() { assertTrue(longBytes.contentEquals(bytes1)) assertTrue(longBytes.contentEquals(bytes2)) + + val bytes3 = t.select { t.id eq id3 }.first()[t.b].inputStream.readBytes() + assertTrue(shortBytes.contentEquals(bytes3)) } } @@ -443,27 +634,29 @@ class DDLTests : DatabaseTestsBase() { val defaultBlobStr = "test" val defaultBlob = ExposedBlob(defaultBlobStr.encodeToByteArray()) - val TestTable = object : Table("TestTable") { + val testTable = object : Table("TestTable") { val number = integer("number") - val blobWithDefault = blob("blobWithDefault").default(defaultBlob) + val blobWithDefault = blob("blobWithDefault") + .default(defaultBlob) } withDb { testDb -> when (testDb) { TestDB.MYSQL -> { expectException { - SchemaUtils.create(TestTable) + SchemaUtils.create(testTable) } } else -> { - SchemaUtils.create(TestTable) + SchemaUtils.drop(testTable) + SchemaUtils.create(testTable) - TestTable.insert { + testTable.insert { it[number] = 1 } - assertEquals(defaultBlobStr, String(TestTable.selectAll().first()[TestTable.blobWithDefault].bytes)) + assertEquals(defaultBlobStr, String(testTable.selectAll().first()[testTable.blobWithDefault].bytes)) - SchemaUtils.drop(TestTable) + SchemaUtils.drop(testTable) } } } @@ -671,66 +864,6 @@ class DDLTests : DatabaseTestsBase() { } } - @Test fun testUByteColumnType() { - val UbyteTable = object : Table("ubyteTable") { - val ubyte = ubyte("ubyte") - } - - withTables(UbyteTable) { - UbyteTable.insert { - it[ubyte] = 123u - } - val result = UbyteTable.selectAll().toList() - assertEquals(1, result.size) - assertEquals(123u, result.single()[UbyteTable.ubyte]) - } - } - - @Test fun testUshortColumnType() { - val UshortTable = object : Table("ushortTable") { - val ushort = ushort("ushort") - } - - withTables(UshortTable) { - UshortTable.insert { - it[ushort] = 123u - } - val result = UshortTable.selectAll().toList() - assertEquals(1, result.size) - assertEquals(123u, result.single()[UshortTable.ushort]) - } - } - - @Test fun testUintColumnType() { - val UintTable = object : Table("uintTable") { - val uint = uinteger("uint") - } - - withTables(UintTable) { - UintTable.insert { - it[uint] = 123u - } - val result = UintTable.selectAll().toList() - assertEquals(1, result.size) - assertEquals(123u, result.single()[UintTable.uint]) - } - } - - @Test fun testUlongColumnType() { - val UlongTable = object : Table("ulongTable") { - val ulong = ulong("ulong") - } - - withTables(UlongTable) { - UlongTable.insert { - it[ulong] = 123uL - } - val result = UlongTable.selectAll().toList() - assertEquals(1, result.size) - assertEquals(123uL, result.single()[UlongTable.ulong]) - } - } - @Test fun tableWithDifferentTextTypes() { val TestTable = object : Table("different_text_column_types") { val id = integer("id").autoIncrement() @@ -793,25 +926,27 @@ class DDLTests : DatabaseTestsBase() { val negative = integer("negative").check("subZero") { it less 0 } } - withTables(listOf(TestDB.MYSQL), checkTable) { - checkTable.insert { - it[positive] = 42 - it[negative] = -14 - } + withTables(checkTable) { + if (!isOldMySql()) { + checkTable.insert { + it[positive] = 42 + it[negative] = -14 + } - assertEquals(1L, checkTable.selectAll().count()) + assertEquals(1L, checkTable.selectAll().count()) - assertFailAndRollback("Check constraint 1") { - checkTable.insert { - it[positive] = -472 - it[negative] = -354 + assertFailAndRollback("Check constraint 1") { + checkTable.insert { + it[positive] = -472 + it[negative] = -354 + } } - } - assertFailAndRollback("Check constraint 2") { - checkTable.insert { - it[positive] = 538 - it[negative] = 915 + assertFailAndRollback("Check constraint 2") { + checkTable.insert { + it[positive] = 538 + it[negative] = 915 + } } } } @@ -827,32 +962,73 @@ class DDLTests : DatabaseTestsBase() { } } - withTables(listOf(TestDB.MYSQL), checkTable) { - checkTable.insert { - it[positive] = 57 - it[negative] = -32 - } + withTables(checkTable) { + if (!isOldMySql()) { + checkTable.insert { + it[positive] = 57 + it[negative] = -32 + } - assertEquals(1L, checkTable.selectAll().count()) + assertEquals(1L, checkTable.selectAll().count()) - assertFailAndRollback("Check constraint 1") { - checkTable.insert { - it[positive] = -47 - it[negative] = -35 + assertFailAndRollback("Check constraint 1") { + checkTable.insert { + it[positive] = -47 + it[negative] = -35 + } + } + + assertFailAndRollback("Check constraint 2") { + checkTable.insert { + it[positive] = 53 + it[negative] = 91 + } } } + } + } - assertFailAndRollback("Check constraint 2") { - checkTable.insert { - it[positive] = 53 - it[negative] = 91 + @Test + fun testCreateAndDropCheckConstraint() { + val tester = object : Table("tester") { + val amount = integer("amount") + } + + withTables(tester) { testDb -> + val constraintName = "check_tester_positive" + val constraintOp = "${"amount".inProperCase()} > 0" + val (createConstraint, dropConstraint) = CheckConstraint("tester", constraintName, constraintOp).run { + createStatement() to dropStatement() + } + + if (testDb == TestDB.SQLITE || isOldMySql()) { // cannot alter existing check constraint + assertTrue(createConstraint.isEmpty() && dropConstraint.isEmpty()) + } else { + val negative = -9 + tester.insert { it[amount] = negative } + + // fails to create check constraint because negative values already stored + assertFailAndRollback("Check constraint violation") { + exec(createConstraint.single()) } + + tester.deleteAll() + exec(createConstraint.single()) + + assertFailAndRollback("Check constraint violation") { + tester.insert { it[amount] = negative } + } + + exec(dropConstraint.single()) + + tester.insert { it[amount] = negative } + assertEquals(negative, tester.selectAll().single()[tester.amount]) } } } @Test - fun testCheckConstraint03() { + fun testEqOperatorWithoutDBConnection() { object : Table("test") { val testColumn: Column = integer("test_column").nullable() @@ -865,7 +1041,7 @@ class DDLTests : DatabaseTestsBase() { } @Test - fun testCheckConstraint04() { + fun testNeqOperatorWithoutDBConnection() { object : Table("test") { val testColumn: Column = integer("test_column").nullable() @@ -890,8 +1066,13 @@ class DDLTests : DatabaseTestsBase() { } } + object KeyWordTable : IntIdTable(name = "keywords") { + val bool = bool("bool") + } + // https://github.com/JetBrains/Exposed/issues/112 - @Test fun testDropTableFlushesCache() { + @Test + fun testDropTableFlushesCache() { withDb { class Keyword(id: EntityID) : IntEntity(id) { var bool by KeyWordTable.bool @@ -926,10 +1107,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 SELECT ON ${TableFromSchemeOne.tableName} to TWO;") + exec("GRANT REFERENCES ON ${TableFromSchemeOne.tableName} to TWO") } + + SchemaUtils.drop(TableFromSchemeTwo) SchemaUtils.create(TableFromSchemeTwo) val idFromOne = TableFromSchemeOne.insertAndGetId { } @@ -1120,8 +1305,6 @@ class DDLTests : DatabaseTestsBase() { if (currentDialectTest is SQLServerDialect) { SchemaUtils.drop(tableA, tableB) } - } } - } 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/NestedTransactionsTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/NestedTransactionsTest.kt index bec4ce2e3f..9442754f47 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/NestedTransactionsTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/NestedTransactionsTest.kt @@ -58,7 +58,8 @@ class NestedTransactionsTest : DatabaseTestsBase() { assertNotNull(TransactionManager.currentOrNull()) try { - inTopLevelTransaction(this.transactionIsolation, 1) { + inTopLevelTransaction(this.transactionIsolation) { + repetitionAttempts = 1 throw IllegalStateException("Should be rethrow") } } catch (e: Exception) { 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 c298f381c5..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 @@ -6,7 +6,6 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.vendors.OracleDialect import org.jetbrains.exposed.sql.vendors.SQLServerDialect import org.jetbrains.exposed.sql.vendors.currentDialect import org.junit.Assume @@ -56,6 +55,20 @@ class SchemaTests : DatabaseTestsBase() { } } + @Test + fun testDropSchemaWithCascade() { + withDb { + if (currentDialect.supportsCreateSchema) { + val schema = Schema("TEST_SCHEMA") + SchemaUtils.createSchema(schema) + assertTrue(schema.exists()) + + SchemaUtils.dropSchema(schema, cascade = true) + assertFalse(schema.exists()) + } + } + } + @Test fun `table references table with same name in other database in mysql`() { withDb(listOf(TestDB.MYSQL, TestDB.MARIADB)) { @@ -65,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 @@ -119,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 75b8efae02..b0ad4e4a97 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,230 +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, 42, db = db) { - exec("SELECT 1;") - // NO OP - } - fail("Should have thrown ${GetConnectException::class.simpleName}") - } catch (e: ExposedSQLException) { - assertTrue(e.cause is GetConnectException) - assertEquals(42, 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 = ConnectionExceptions.WrappingDataSource(TestDB.H2, connectionDecorator) - val db = Database.connect(datasource = wrappingDataSource) - try { - transaction(Connection.TRANSACTION_SERIALIZABLE, 5, db = db) { - 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, 5, db = db) { - 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 = ConnectionExceptions.WrappingDataSource(TestDB.H2, connectionDecorator) - val db = Database.connect(datasource = wrappingDataSource) - try { - transaction(Connection.TRANSACTION_SERIALIZABLE, 5, db = db) { - 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() @@ -268,7 +56,8 @@ class ThreadLocalManagerTest : DatabaseTestsBase() { ) withTables(excludeSettings = excludeSettings, RollbackTable) { assertFails { - inTopLevelTransaction(db.transactionManager.defaultIsolationLevel, 1, true) { + inTopLevelTransaction(db.transactionManager.defaultIsolationLevel, true) { + repetitionAttempts = 1 RollbackTable.insert { it[value] = "random-something" } } }.message?.run { assertTrue(contains("read-only")) } ?: fail("message should not be null") @@ -276,90 +65,6 @@ class ThreadLocalManagerTest : DatabaseTestsBase() { } } -object RollbackTable : IntIdTable() { +object RollbackTable : IntIdTable("rollbackTable") { val value = varchar("value", 20) } - -class RollbackTransactionTest : DatabaseTestsBase() { - - @Test - fun testRollbackWithoutSavepoints() { - withTables(RollbackTable) { - inTopLevelTransaction(db.transactionManager.defaultIsolationLevel, 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, 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, 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 97bdb6b33c..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,20 +3,21 @@ 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() { @Test - fun `create database test`() { - // PostgreSQL will be tested in the next test function - withDb(excludeSettings = listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG)) { + fun testCreateAndDropDatabase() { + withDb(excludeSettings = listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.ORACLE)) { val dbName = "jetbrains" try { SchemaUtils.dropDatabase(dbName) - } catch (e: SQLException) { - //ignore + } catch (cause: SQLException) { + // ignore } SchemaUtils.createDatabase(dbName) SchemaUtils.dropDatabase(dbName) @@ -24,7 +25,54 @@ class CreateDatabaseTest : DatabaseTestsBase() { } @Test - fun `create database test in postgreSQL`() { + 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 d8a58bdaed..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 @@ -1,14 +1,20 @@ package org.jetbrains.exposed.sql.tests.shared.ddl -import org.jetbrains.exposed.sql.Schema -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.exists +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus 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.tests.shared.assertTrue +import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect +import org.jetbrains.exposed.sql.vendors.SQLServerDialect +import org.jetbrains.exposed.sql.vendors.SQLiteDialect +import org.jetbrains.exposed.sql.vendors.currentDialect import org.junit.Test +import kotlin.test.expect class CreateIndexTests : DatabaseTestsBase() { @@ -39,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()) } @@ -63,8 +72,8 @@ class CreateIndexTests : DatabaseTestsBase() { } @Test - fun `test possibility to create indexes when table exists in different schemas`() { - val TestTable = object : Table("test_table") { + fun testCreateIndexWithTableInDifferentSchemas() { + val testTable = object : Table("test_table") { val id = integer("id").uniqueIndex() val name = varchar("name", length = 42).index("test_index") init { @@ -75,12 +84,209 @@ class CreateIndexTests : DatabaseTestsBase() { val schema2 = Schema("Schema2") withSchemas(listOf(TestDB.SQLITE, TestDB.SQLSERVER), schema1, schema2) { SchemaUtils.setSchema(schema1) - SchemaUtils.createMissingTablesAndColumns(TestTable) - assertEquals(true, TestTable.exists()) + SchemaUtils.createMissingTablesAndColumns(testTable) + assertEquals(true, testTable.exists()) SchemaUtils.setSchema(schema2) - assertEquals(false, TestTable.exists()) - SchemaUtils.createMissingTablesAndColumns(TestTable) - assertEquals(true, TestTable.exists()) + assertEquals(false, testTable.exists()) + SchemaUtils.createMissingTablesAndColumns(testTable) + assertEquals(true, testTable.exists()) + } + } + + @Test + fun testCreateAndDropPartialIndexWithPostgres() { + val partialIndexTable = object : IntIdTable("PartialIndexTableTest") { + val name = varchar("name", 50) + val value = integer("value") + val anotherValue = integer("anotherValue") + val flag = bool("flag") + init { + index("flag_index", columns = arrayOf(flag, name)) { + flag eq true + } + index(columns = arrayOf(value, name)) { + (name eq "aaa") and (value greaterEq 6) + } + uniqueIndex(columns = arrayOf(anotherValue)) + } } + + withDb(listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG)) { + SchemaUtils.createMissingTablesAndColumns(partialIndexTable) + assertTrue(partialIndexTable.exists()) + + // check that indexes are created and contain the proper filtering conditions + exec( + """SELECT indexname AS INDEX_NAME, + substring(indexdef, strpos(indexdef, ' WHERE ') + 7) AS FILTER_CONDITION + FROM pg_indexes + WHERE tablename='partialindextabletest' AND indexname != 'partialindextabletest_pkey' + """.trimIndent() + ) { + var totalIndexCount = 0 + while (it.next()) { + totalIndexCount += 1 + val filter = it.getString("FILTER_CONDITION") + + when (it.getString("INDEX_NAME")) { + "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 ")) + } + } + 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" + ) + + execInBatch(listOf(dropUniqueConstraint, dropIndex)) + + assertEquals(getIndices(partialIndexTable).size, 1) + SchemaUtils.drop(partialIndexTable) + } + } + + @Test + fun testCreateAndDropPartialIndex() { + val tester = object : Table("tester") { + val name = varchar("name", 32).uniqueIndex() + val age = integer("age") + val team = varchar("team", 32) + + init { + uniqueIndex("team_only_index", team) { team eq "A" } + index("name_age_index", isUnique = false, name, age) { age greaterEq 20 } + } + } + + withDb(listOf(TestDB.SQLITE, TestDB.SQLSERVER, TestDB.POSTGRESQLNG, TestDB.POSTGRESQL)) { + SchemaUtils.createMissingTablesAndColumns(tester) + assertTrue(tester.exists()) + + val createdStatements = tester.indices.map { SchemaUtils.createIndex(it).first() } + assertEquals(3, createdStatements.size) + if (currentDialectTest is SQLiteDialect) { + assertTrue(createdStatements.all { it.startsWith("CREATE ") }) + } else { + assertEquals(2, createdStatements.count { it.startsWith("CREATE ") }) + assertEquals(1, createdStatements.count { it.startsWith("ALTER TABLE ") }) + } + + assertEquals(2, tester.indices.count { it.filterCondition != null }) + + var indices = getIndices(tester) + assertEquals(3, indices.size) + + 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) } + + indices = getIndices(tester) + assertEquals(0, indices.size) + + // test for non-unique partial index with type + val type: String? = when (currentDialectTest) { + is PostgreSQLDialect -> "BTREE" + is SQLServerDialect -> "NONCLUSTERED" + else -> null + } + val typedPartialIndex = Index( + listOf(tester.name), + false, + "name_only_index", + type, + tester.name neq "Default" + ) + val createdIndex = SchemaUtils.createIndex(typedPartialIndex).single() + assertTrue(createdIndex.startsWith("CREATE ")) + assertTrue(" WHERE " in createdIndex) + assertTrue(typedPartialIndex.dropStatement().first().startsWith("DROP INDEX ")) + + SchemaUtils.drop(tester) + } + } + + @Test + fun testPartialIndexNotCreated() { + val tester = object : Table("tester") { + val age = integer("age") + + init { + index("age_index", false, age) { age greaterEq 10 } + } + } + + withTables(tester) { + SchemaUtils.createMissingTablesAndColumns() + assertTrue(tester.exists()) + + val expectedIndexCount = when (currentDialectTest) { + is PostgreSQLDialect, is SQLServerDialect, is SQLiteDialect -> 1 + else -> 0 + } + val actualIndexCount = currentDialectTest.existingIndices(tester)[tester].orEmpty().size + assertEquals(expectedIndexCount, actualIndexCount) + } + } + + @Test + fun testCreateAndDropFunctionalIndex() { + val tester = object : IntIdTable("tester") { + val amount = integer("amount") + val price = integer("price") + val item = varchar("item", 32).nullable() + + init { + index(customIndexName = "tester_plus_index", isUnique = false, functions = listOf(amount.plus(price))) + index(isUnique = false, functions = listOf(item.lowerCase())) + uniqueIndex(columns = arrayOf(price), functions = listOf(Coalesce(item, stringLiteral("*")))) + } + } + + val functionsNotSupported = TestDB.allH2TestDB + TestDB.SQLSERVER + TestDB.MARIADB + withTables(excludeSettings = functionsNotSupported, tester) { + if (!isOldMySql()) { + SchemaUtils.createMissingTablesAndColumns() + assertTrue(tester.exists()) + + var indices = getIndices(tester) + assertEquals(3, indices.size) + + val dropStatements = indices.map { it.dropStatement().first() } + expect(Unit) { execInBatch(dropStatements) } + + indices = getIndices(tester) + assertEquals(0, indices.size) + } + } + } + + private fun Transaction.getIndices(table: Table): List { + db.dialect.resetCaches() + return currentDialect.existingIndices(table)[table].orEmpty() } } 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 c6ad0f56a8..180323d9ed 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 @@ -3,6 +3,7 @@ package org.jetbrains.exposed.sql.tests.shared.ddl import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull import org.jetbrains.exposed.sql.tests.DatabaseTestsBase @@ -16,10 +17,13 @@ import org.jetbrains.exposed.sql.tests.shared.assertFailAndRollback import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.OracleDialect +import org.jetbrains.exposed.sql.vendors.PrimaryKeyMetadata import org.junit.Test import java.math.BigDecimal -import java.util.* +import java.util.UUID import kotlin.properties.Delegates +import kotlin.test.assertNotNull +import kotlin.test.assertNull class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { @@ -52,9 +56,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { override val primaryKey = PrimaryKey(id) } - withDb { dbSetting -> - val tooOldMysql = dbSetting == TestDB.MYSQL && !db.isVersionCovers(BigDecimal("5.6")) - if (!tooOldMysql) { + withDb { + if (!isOldMySql()) { SchemaUtils.createMissingTablesAndColumns(TestTable) assertTrue(TestTable.exists()) try { @@ -185,7 +188,6 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } withDb { -// withDb(db = listOf(TestDB.H2)) { SchemaUtils.createMissingTablesAndColumns(t1) val missingStatements = SchemaUtils.addMissingColumnsStatements(t2) @@ -211,7 +213,6 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } withDb { -// withDb(db = listOf(TestDB.H2)) { SchemaUtils.createMissingTablesAndColumns(t1) val missingStatements = SchemaUtils.addMissingColumnsStatements(t2) @@ -241,7 +242,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } } - @Test fun addAutoPrimaryKey() { + @Test + fun addAutoPrimaryKey() { val tableName = "Foo" val initialTable = object : Table(tableName) { val bar = text("bar") @@ -256,13 +258,44 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } } + @Test + fun testAddNewPrimaryKeyOnExistingColumn() { + val tableName = "tester" + val noPKTable = object : Table(tableName) { + val bar = integer("bar") + } + + val singlePKTable = object : Table(tableName) { + val bar = integer("bar") + + override val primaryKey = PrimaryKey(bar) + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { + SchemaUtils.createMissingTablesAndColumns(noPKTable) + var primaryKey: PrimaryKeyMetadata? = currentDialectTest.existingPrimaryKeys(singlePKTable)[singlePKTable] + assertNull(primaryKey) + + val expected = "ALTER TABLE ${tableName.inProperCase()} ADD PRIMARY KEY (${noPKTable.bar.nameInDatabaseCase()})" + val statements = SchemaUtils.statementsRequiredToActualizeScheme(singlePKTable) + assertEquals(expected, statements.single()) + + SchemaUtils.createMissingTablesAndColumns(singlePKTable) + primaryKey = currentDialectTest.existingPrimaryKeys(singlePKTable)[singlePKTable] + assertNotNull(primaryKey) + assertEquals("bar".inProperCase(), primaryKey.columnNames.single()) + + SchemaUtils.drop(noPKTable) + } + } + @Test fun `columns with default values that haven't changed shouldn't trigger change`() { var table by Delegates.notNull
() withDb { testDb -> try { // MySQL doesn't support default values on text columns, hence excluded - table = if(testDb != TestDB.MYSQL) { + table = if (testDb != TestDB.MYSQL) { object : Table("varchar_test") { val varchar = varchar("varchar_column", 255).default(" ") val text = text("text_column").default(" ") @@ -296,7 +329,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { @Test fun `columns with default values that are whitespaces shouldn't be treated as empty strings`() { - val tableWhitespaceDefaultVarchar = StringFieldTable("varchar_whitespace_test", false," ") + val tableWhitespaceDefaultVarchar = StringFieldTable("varchar_whitespace_test", false, " ") val tableWhitespaceDefaultText = StringFieldTable("text_whitespace_test", true, " ") @@ -321,11 +354,11 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { assertEquals(" ", whiteSpaceTable.select { whiteSpaceTable.id eq whiteSpaceId }.single()[whiteSpaceTable.column]) val actual = SchemaUtils.statementsRequiredToActualizeScheme(emptyTable) - assertEquals(1, actual.size) + val expected = if (testDb == TestDB.SQLSERVER) 2 else 1 + assertEquals(expected, actual.size) - // SQL Server requires drop/create constraint to change defaults, unsupported for now // Oracle treat '' as NULL column and can't alter from NULL to NULL - if (testDb !in listOf(TestDB.SQLSERVER, TestDB.ORACLE)) { + if (testDb != TestDB.ORACLE) { // Apply changes actual.forEach { exec(it) } } else { @@ -367,8 +400,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { override val primaryKey = PrimaryKey(id) } - val excludeSettings = listOf(TestDB.SQLITE, TestDB.SQLSERVER) - val complexAlterTable = listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.ORACLE, TestDB.H2_PSQL) + val excludeSettings = listOf(TestDB.SQLITE) + val complexAlterTable = listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.ORACLE, TestDB.H2_PSQL, TestDB.SQLSERVER) withDb(excludeSettings = excludeSettings) { testDb -> try { SchemaUtils.createMissingTablesAndColumns(t1) @@ -488,7 +521,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { val traceNumber = reference("traceNumber", ordersTable.traceNumber) } - withDb { + // Oracle metadata only returns foreign keys that reference primary keys + withDb(excludeSettings = listOf(TestDB.ORACLE)) { SchemaUtils.createMissingTablesAndColumns(ordersTable, receiptsTable) assertTrue(ordersTable.exists()) assertTrue(receiptsTable.exists()) @@ -506,7 +540,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { } } - @Test fun testCreateTableWithReferenceMultipleTimes() { + @Test + fun testCreateTableWithReferenceMultipleTimes() { withTables(PlayerTable, SessionsTable) { SchemaUtils.createMissingTablesAndColumns(PlayerTable, SessionsTable) SchemaUtils.createMissingTablesAndColumns(PlayerTable, SessionsTable) @@ -521,7 +556,8 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { val playerId = integer("player_id").references(PlayerTable.id) } - @Test fun createTableWithReservedIdentifierInColumnName() { + @Test + fun createTableWithReservedIdentifierInColumnName() { withDb(TestDB.MYSQL) { SchemaUtils.createMissingTablesAndColumns(T1, T2) SchemaUtils.createMissingTablesAndColumns(T1, T2) @@ -534,11 +570,13 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { object ExplicitTable : IntIdTable() { val playerId = integer("player_id").references(PlayerTable.id, fkName = "Explicit_FK_NAME") } + object NonExplicitTable : IntIdTable() { val playerId = integer("player_id").references(PlayerTable.id) } - @Test fun explicitFkNameIsExplicit() { + @Test + fun explicitFkNameIsExplicit() { withTables(ExplicitTable, NonExplicitTable) { assertEquals("Explicit_FK_NAME", ExplicitTable.playerId.foreignKey!!.customFkName) assertEquals(null, NonExplicitTable.playerId.foreignKey!!.customFkName) @@ -549,6 +587,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { val name = integer("name").uniqueIndex() val tmp = varchar("temp", 255) } + object T2 : Table("CHAIN") { val ref = integer("ref").references(T1.name) } @@ -592,4 +631,74 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { SchemaUtils.createMissingTablesAndColumns(CompositePrimaryKeyTable, CompositeForeignKeyTable) } } + + @Test + fun testCreateTableWithQuotedIdentifiers() { + val identifiers = listOf("\"IdentifierTable\"", "\"IDentiFierCoLUmn\"") + val quotedTable = object : Table(identifiers[0]) { + val column1 = varchar(identifiers[1], 32) + } + + withDb { + try { + SchemaUtils.createMissingTablesAndColumns(quotedTable) + assertTrue(quotedTable.exists()) + + val statements = SchemaUtils.statementsRequiredToActualizeScheme(quotedTable) + assertTrue(statements.isEmpty()) + } finally { + SchemaUtils.drop(quotedTable) + } + } + } + + @Test + fun testCreateCompositePrimaryKeyTableAndCompositeForeignKeyInVariousOrder() { + withTables(CompositeForeignKeyTable, CompositePrimaryKeyTable) { + SchemaUtils.createMissingTablesAndColumns(CompositePrimaryKeyTable, CompositeForeignKeyTable) + } + withTables(CompositeForeignKeyTable, CompositePrimaryKeyTable) { + SchemaUtils.createMissingTablesAndColumns(CompositeForeignKeyTable, CompositePrimaryKeyTable) + } + withTables(CompositePrimaryKeyTable, CompositeForeignKeyTable) { + SchemaUtils.createMissingTablesAndColumns(CompositePrimaryKeyTable, CompositeForeignKeyTable) + } + withTables(CompositePrimaryKeyTable, CompositeForeignKeyTable) { + SchemaUtils.createMissingTablesAndColumns(CompositeForeignKeyTable, CompositePrimaryKeyTable) + } + } + + @Test + fun testCreateTableWithSchemaPrefix() { + val schemaName = "my_schema" + val schema = Schema(schemaName) + // index and foreign key both use table name to auto-generate their own names & to compare metadata + // default columns in SQL Server requires a named constraint that uses table name + val parentTable = object : IntIdTable("$schemaName.parent_table") { + val secondId = integer("second_id").uniqueIndex() + val column1 = varchar("column_1", 32).default("TEST") + } + val childTable = object : LongIdTable("$schemaName.child_table") { + val parent = reference("my_parent", parentTable) + } + + // SQLite does not recognize creation of schema other than the attached database + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + SchemaUtils.createSchema(schema) + SchemaUtils.create(parentTable, childTable) + + try { + SchemaUtils.createMissingTablesAndColumns(parentTable, childTable) + assertTrue(parentTable.exists()) + assertTrue(childTable.exists()) + } finally { + if (testDb == TestDB.SQLSERVER) { + SchemaUtils.drop(childTable, parentTable) + SchemaUtils.dropSchema(schema) + } else { + SchemaUtils.dropSchema(schema, cascade = true) + } + } + } + } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt index ea243fbee8..8e9c0efa98 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt @@ -8,6 +8,8 @@ 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.inProperCase +import org.jetbrains.exposed.sql.tests.shared.Category +import org.jetbrains.exposed.sql.tests.shared.Item import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.assertTrue @@ -333,17 +335,14 @@ class CreateTableTests : DatabaseTestsBase() { onDelete = ReferenceOption.NO_ACTION, ) } - withTables(excludeSettings = listOf(TestDB.H2_ORACLE), parent, child) { - // Different dialects use different mix of lowercase/uppercase in their names - val expected = listOf( - "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(child)} (" + - "${child.columns.joinToString { it.descriptionDdl(false) }}," + - " CONSTRAINT ${"fk_Child_parent_id__id".inProperCase()}" + - " FOREIGN KEY (${this.identity(child.parentId)})" + - " REFERENCES ${this.identity(parent)}(${this.identity(parent.id)})" + - ")" - ) - assertEqualCollections(child.ddl, expected) + withTables(parent, child) { + val expected = "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(child)} (" + + "${child.columns.joinToString { it.descriptionDdl(false) }}," + + " CONSTRAINT ${"fk_Child_parent_id__id".inProperCase()}" + + " FOREIGN KEY (${this.identity(child.parentId)})" + + " REFERENCES ${this.identity(parent)}(${this.identity(parent.id)})" + + ")" + assertEquals(child.ddl.last(), expected) } } @@ -358,17 +357,14 @@ class CreateTableTests : DatabaseTestsBase() { onDelete = ReferenceOption.NO_ACTION, ) } - withTables(excludeSettings = listOf(TestDB.H2_ORACLE), parent, child) { - // Different dialects use different mix of lowercase/uppercase in their names - val expected = listOf( - "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(child)} (" + - "${child.columns.joinToString { it.descriptionDdl(false) }}," + - " CONSTRAINT ${"fk_Child2_parent_id__id".inProperCase()}" + - " FOREIGN KEY (${this.identity(child.parentId)})" + - " REFERENCES ${this.identity(parent)}(${this.identity(parent.id)})" + - ")" - ) - assertEqualCollections(child.ddl, expected) + withTables(parent, child) { + val expected = "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(child)} (" + + "${child.columns.joinToString { it.descriptionDdl(false) }}," + + " CONSTRAINT ${"fk_Child2_parent_id__id".inProperCase()}" + + " FOREIGN KEY (${this.identity(child.parentId)})" + + " REFERENCES ${this.identity(parent)}(${this.identity(parent.id)})" + + ")" + assertEquals(child.ddl.last(), expected) } } @@ -506,7 +502,7 @@ class CreateTableTests : DatabaseTestsBase() { } withTables(parent, child) { testDb -> val t = TransactionManager.current() - val updateCascadePart = if (testDb !in listOf(TestDB.ORACLE, TestDB.H2_ORACLE)) " ON UPDATE CASCADE" else "" + val updateCascadePart = if (testDb != TestDB.ORACLE) " ON UPDATE CASCADE" else "" val expected = listOfNotNull( child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { Sequence( @@ -533,6 +529,7 @@ class CreateTableTests : DatabaseTestsBase() { val parent = object : Table("parent2") { val idA = integer("id_a") val idB = integer("id_b") + init { uniqueIndex(idA, idB) } @@ -572,6 +569,23 @@ class CreateTableTests : DatabaseTestsBase() { } } + @Test + fun createTableWithOnDeleteSetDefault() { + withDb(excludeSettings = listOf(TestDB.MARIADB, TestDB.MYSQL, TestDB.ORACLE)) { + val expected = listOf( + "CREATE TABLE " + addIfNotExistsIfSupported() + "${this.identity(Item)} (" + + "${Item.columns.joinToString { it.descriptionDdl(false) }}," + + " CONSTRAINT ${"fk_Item_categoryId__id".inProperCase()}" + + " FOREIGN KEY (${this.identity(Item.categoryId)})" + + " REFERENCES ${this.identity(Category)}(${this.identity(Category.id)})" + + " ON DELETE SET DEFAULT" + + ")" + ) + + assertEqualCollections(Item.ddl, expected) + } + } + object OneTable : IntIdTable("one") object OneOneTable : IntIdTable("one.one") @@ -598,7 +612,8 @@ class CreateTableTests : DatabaseTestsBase() { } } - @Test fun `create table with quoted name with camel case`() { + @Test + fun `create table with quoted name with camel case`() { val testTable = object : IntIdTable("quotedTable") { val int = integer("intColumn") } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/EnumerationTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/EnumerationTests.kt index 59b3613b66..6b9285a7d9 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/EnumerationTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/EnumerationTests.kt @@ -16,6 +16,8 @@ import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect import org.junit.Test class EnumerationTests : DatabaseTestsBase() { + private val supportsCustomEnumerationDB = TestDB.mySqlRelatedDB + listOf(TestDB.H2, TestDB.H2_PSQL, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG) + object EnumTable : IntIdTable("EnumTable") { internal var enumColumn: Column = enumeration("enumColumn") @@ -41,7 +43,7 @@ class EnumerationTests : DatabaseTestsBase() { @Test fun testCustomEnumeration01() { - withDb(listOf(TestDB.H2, TestDB.H2_PSQL, TestDB.MYSQL, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG)) { + withDb(supportsCustomEnumerationDB) { val sqlType = when (currentDialectTest) { is H2Dialect, is MysqlDialect -> "ENUM('Bar', 'Baz')" is PostgreSQLDialect -> "FooEnum" @@ -61,6 +63,10 @@ class EnumerationTests : DatabaseTestsBase() { } EnumTable.initEnumColumn(sqlType) SchemaUtils.create(EnumTable) + // drop shared table object's unique index if created in other test + if (EnumTable.indices.isNotEmpty()) { + exec(EnumTable.indices.first().dropStatement().single()) + } EnumTable.insert { it[enumColumn] = DDLTests.Foo.Bar } @@ -90,7 +96,7 @@ class EnumerationTests : DatabaseTestsBase() { @Test fun testCustomEnumerationWithDefaultValue() { - withDb(listOf(TestDB.H2, TestDB.H2_MYSQL, TestDB.H2_PSQL, TestDB.MYSQL, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG)) { + withDb(supportsCustomEnumerationDB) { val sqlType = when (currentDialectTest) { is H2Dialect, is MysqlDialect -> "ENUM('Bar', 'Baz')" is PostgreSQLDialect -> "FooEnum2" @@ -103,9 +109,13 @@ class EnumerationTests : DatabaseTestsBase() { } EnumTable.initEnumColumn(sqlType) with(EnumTable) { - EnumTable.enumColumn.default(DDLTests.Foo.Bar) + enumColumn.default(DDLTests.Foo.Bar) } SchemaUtils.create(EnumTable) + // drop shared table object's unique index if created in other test + if (EnumTable.indices.isNotEmpty()) { + exec(EnumTable.indices.first().dropStatement().single()) + } EnumTable.insert { } val default = EnumTable.selectAll().single()[EnumTable.enumColumn] @@ -117,4 +127,84 @@ class EnumerationTests : DatabaseTestsBase() { } } } + + @Test + fun testCustomEnumerationWithReference() { + val referenceTable = object : Table("ref_table") { + var referenceColumn: Column = enumeration("ref_column") + + fun initRefColumn() { + (columns as MutableList>).remove(referenceColumn) + referenceColumn = reference("ref_column", EnumTable.enumColumn) + } + } + + withDb(supportsCustomEnumerationDB) { + val sqlType = when (currentDialectTest) { + is H2Dialect, is MysqlDialect -> "ENUM('Bar', 'Baz')" + is PostgreSQLDialect -> "RefEnum" + else -> error("Unsupported case") + } + try { + if (currentDialectTest is PostgreSQLDialect) { + exec("DROP TYPE IF EXISTS $sqlType;") + exec("CREATE TYPE $sqlType AS ENUM ('Bar', 'Baz');") + } + EnumTable.initEnumColumn(sqlType) + with(EnumTable) { + if (indices.isEmpty()) enumColumn.uniqueIndex() + } + SchemaUtils.create(EnumTable) + + referenceTable.initRefColumn() + SchemaUtils.create(referenceTable) + + val fooBar = DDLTests.Foo.Bar + val id1 = EnumTable.insert { + it[enumColumn] = fooBar + } get EnumTable.enumColumn + referenceTable.insert { + it[referenceColumn] = id1 + } + + assertEquals(fooBar, EnumTable.selectAll().single()[EnumTable.enumColumn]) + assertEquals(fooBar, referenceTable.selectAll().single()[referenceTable.referenceColumn]) + } finally { + SchemaUtils.drop(referenceTable) + exec(EnumTable.indices.first().dropStatement().single()) + SchemaUtils.drop(EnumTable) + } + } + } + + @Test + fun testEnumerationColumnsWithReference() { + val tester = object : Table("tester") { + val enumColumn = enumeration("enum_column").uniqueIndex() + val enumNameColumn = enumerationByName("enum_name_column", 32).uniqueIndex() + } + val referenceTable = object : Table("ref_table") { + val referenceColumn = reference("ref_column", tester.enumColumn) + val referenceNameColumn = reference("ref_name_column", tester.enumNameColumn) + } + + withTables(tester, referenceTable) { + val fooBar = DDLTests.Foo.Bar + val fooBaz = DDLTests.Foo.Baz + val entry = tester.insert { + it[enumColumn] = fooBar + it[enumNameColumn] = fooBaz + } + referenceTable.insert { + it[referenceColumn] = entry[tester.enumColumn] + it[referenceNameColumn] = entry[tester.enumNameColumn] + } + + assertEquals(fooBar, tester.selectAll().single()[tester.enumColumn]) + assertEquals(fooBar, referenceTable.selectAll().single()[referenceTable.referenceColumn]) + + assertEquals(fooBaz, tester.selectAll().single()[tester.enumNameColumn]) + assertEquals(fooBaz, referenceTable.selectAll().single()[referenceTable.referenceNameColumn]) + } + } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ConditionsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ConditionsTests.kt index f68c38c046..47e2ed3fb8 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ConditionsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ConditionsTests.kt @@ -4,6 +4,7 @@ import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.exceptions.ExposedSQLException 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.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.expectException @@ -13,9 +14,9 @@ class ConditionsTests : DatabaseTestsBase() { @Test fun testTRUEandFALSEOps() { withCitiesAndUsers { cities, _, _ -> - val allSities = cities.selectAll().toCityNameList() + val allCities = cities.selectAll().toCityNameList() assertEquals(0L, cities.select { Op.FALSE }.count()) - assertEquals(allSities.size.toLong(), cities.select { Op.TRUE }.count()) + assertEquals(allCities.size.toLong(), cities.select { Op.TRUE }.count()) } } @@ -25,8 +26,8 @@ class ConditionsTests : DatabaseTestsBase() { val number1 = integer("number_1").nullable() val number2 = integer("number_2").nullable() } - - withTables(table) { + // remove SQL Server exclusion once test container supports SQL Server 2022 + withTables(excludeSettings = listOf(TestDB.SQLSERVER), table) { val sameNumberId = table.insert { it[number1] = 0 it[number2] = 0 @@ -158,9 +159,9 @@ class ConditionsTests : DatabaseTestsBase() { @Test fun nullOpInCaseTest() { withCitiesAndUsers { cities, _, _ -> - val caseCondition = Case(). - When(Op.build { cities.id eq 1 }, Op.nullOp()). - Else(cities.name) + val caseCondition = Case() + .When(Op.build { cities.id eq 1 }, Op.nullOp()) + .Else(cities.name) var nullBranchWasExecuted = false cities.slice(cities.id, cities.name, caseCondition).selectAll().forEach { val result = it[caseCondition] @@ -174,4 +175,65 @@ class ConditionsTests : DatabaseTestsBase() { assertEquals(true, nullBranchWasExecuted) } } + + @Test + fun testCaseWhenElseAsArgument() { + withCitiesAndUsers { cities, _, _ -> + val original = "ORIGINAL" + val copy = "COPY" + val condition = Op.build { cities.id eq 1 } + + val caseCondition1 = Case() + .When(condition, stringLiteral(original)) + .Else(Op.nullOp()) + // Case().When().Else() invokes CaseWhenElse() so the 2 formats should be interchangeable as arguments + val caseCondition2 = CaseWhenElse( + Case().When(condition, stringLiteral(original)), + Op.nullOp() + ) + val function1 = Coalesce(caseCondition1, stringLiteral(copy)) + val function2 = Coalesce(caseCondition2, stringLiteral(copy)) + + // confirm both formats produce identical SQL + val query1 = cities.slice(cities.id, function1).selectAll().prepareSQL(this, prepared = false) + val query2 = cities.slice(cities.id, function2).selectAll().prepareSQL(this, prepared = false) + assertEquals(query1, query2) + + val results1 = cities.slice(cities.id, function1).selectAll().toList() + cities.slice(cities.id, function2).selectAll().forEachIndexed { i, row -> + val currentId = row[cities.id] + val functionResult = row[function2] + + assertEquals(if (currentId == 1) original else copy, functionResult) + assertEquals(currentId, results1[i][cities.id]) + assertEquals(functionResult, results1[i][function1]) + } + } + } + + @Test + fun testChainedAndNestedCaseWhenElseSyntax() { + withCitiesAndUsers { cities, _, _ -> + val nestedCondition = Case() + .When(Op.build { cities.id eq 1 }, intLiteral(1)) + .Else(intLiteral(-1)) + val chainedCondition = Case() + .When(Op.build { cities.name like "M%" }, intLiteral(0)) + .When(Op.build { cities.name like "St. %" }, nestedCondition) + .When(Op.build { cities.name like "P%" }, intLiteral(2)) + .Else(intLiteral(-1)) + + val results = cities.slice(cities.name, chainedCondition).selectAll() + results.forEach { + val cityName = it[cities.name] + val expectedNumber = when { + cityName.startsWith("M") -> 0 + cityName.startsWith("St. ") -> 1 + cityName.startsWith("P") -> 2 + else -> -1 + } + assertEquals(expectedNumber, it[chainedCondition]) + } + } + } } 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 2700a8bdf8..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 @@ -11,6 +11,7 @@ import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB +import java.math.BigDecimal import java.util.* object DMLTestsData { @@ -38,12 +39,23 @@ object DMLTestsData { val comment: Column = varchar("comment", 30) val value: Column = integer("value") } + + object Sales : Table() { + val year: Column = integer("year") + val month: Column = integer("month") + val product: Column = varchar("product", 30).nullable() + val amount: Column = decimal("amount", 8, 2) + } } @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 @@ -125,6 +137,34 @@ fun DatabaseTestsBase.withCitiesAndUsers( } } +fun DatabaseTestsBase.withSales( + statement: Transaction.(testDb: TestDB, sales: DMLTestsData.Sales) -> Unit +) { + val sales = DMLTestsData.Sales + + withTables(sales) { + insertSale(2018, 11, "tea", "550.10") + insertSale(2018, 12, "coffee", "1500.25") + insertSale(2018, 12, "tea", "900.30") + insertSale(2019, 1, "coffee", "1620.10") + insertSale(2019, 1, "tea", "650.70") + insertSale(2019, 2, "coffee", "1870.90") + insertSale(2019, 2, null, "10.20") + + statement(it, sales) + } +} + +private fun insertSale(year: Int, month: Int, product: String?, amount: String) { + val sales = DMLTestsData.Sales + sales.insert { + it[sales.year] = year + it[sales.month] = month + it[sales.product] = product + it[sales.amount] = BigDecimal(amount) + } +} + object OrgMemberships : IntIdTable() { val orgId = reference("org", Orgs.uid) } 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 025c472ecc..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 @@ -8,9 +8,10 @@ import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.statements.BatchInsertStatement 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.currentTestDB import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.assertFailAndRollback @@ -18,10 +19,8 @@ import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.tests.shared.entities.EntityTests import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.junit.Assume import org.junit.Test -import java.math.BigDecimal import java.sql.SQLException import java.util.* import kotlin.test.assertEquals @@ -57,7 +56,7 @@ class InsertTests : DatabaseTestsBase() { } } - private val insertIgnoreSupportedDB = TestDB.values().toList() - + private val insertIgnoreUnsupportedDB = TestDB.values().toList() - listOf(TestDB.SQLITE, TestDB.MYSQL, TestDB.H2_MYSQL, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.H2_PSQL) @Test @@ -66,7 +65,7 @@ class InsertTests : DatabaseTestsBase() { val name = varchar("foo", 10).uniqueIndex() } - withTables(insertIgnoreSupportedDB, idTable) { + withTables(excludeSettings = insertIgnoreUnsupportedDB, idTable) { idTable.insertIgnoreAndGetId { it[idTable.name] = "1" } @@ -141,10 +140,7 @@ class InsertTests : DatabaseTestsBase() { val name = varchar("foo", 10).uniqueIndex() } - val insertIgnoreSupportedDB = TestDB.values().toList() - - listOf(TestDB.SQLITE, TestDB.MYSQL, TestDB.H2_MYSQL, TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.H2_PSQL) - - withTables(insertIgnoreSupportedDB, idTable) { + withTables(excludeSettings = insertIgnoreUnsupportedDB, idTable) { val insertedStatement = idTable.insertIgnore { it[idTable.id] = EntityID(1, idTable) it[idTable.name] = "1" @@ -280,7 +276,6 @@ class InsertTests : DatabaseTestsBase() { } @Test fun testInsertWithExpression() { - val tbl = object : IntIdTable("testInsert") { val nullableInt = integer("nullableIntCol").nullable() val string = varchar("stringCol", 20) @@ -317,7 +312,6 @@ class InsertTests : DatabaseTestsBase() { } @Test fun testInsertWithColumnExpression() { - val tbl1 = object : IntIdTable("testInsert1") { val string1 = varchar("stringCol", 20) } @@ -359,7 +353,6 @@ class InsertTests : DatabaseTestsBase() { // https://github.com/JetBrains/Exposed/issues/192 @Test fun testInsertWithColumnNamedWithKeyword() { withTables(OrderedDataTable) { - val foo = OrderedData.new { name = "foo" order = 20 @@ -380,8 +373,7 @@ class InsertTests : DatabaseTestsBase() { val emojis = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDC67\uD83C\uDFFF\u200D\uD83D\uDC66\uD83C\uDFFF" withTables(TestDB.allH2TestDB + TestDB.SQLSERVER + TestDB.ORACLE, table) { - val isOldMySQL = currentDialectTest is MysqlDialect && db.isVersionCovers(BigDecimal("5.5")) - if (isOldMySQL) { + if (isOldMySql()) { exec("ALTER TABLE ${table.nameInDatabaseCase()} DEFAULT CHARSET utf8mb4, MODIFY emoji VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;") } table.insert { @@ -469,11 +461,12 @@ class InsertTests : DatabaseTestsBase() { } } - @Test fun `rollback on constraint exception normal transactions`() { - val TestTable = object : IntIdTable("TestRollback") { + @Test + fun testRollbackOnConstraintExceptionWithNormalTransactions() { + 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 } ) @@ -482,16 +475,16 @@ class InsertTests : DatabaseTestsBase() { try { try { withDb(db) { - SchemaUtils.create(TestTable) - TestTable.insert { it[foo] = 1 } - TestTable.insert { it[foo] = 0 } + SchemaUtils.create(testTable) + testTable.insert { it[foo] = 1 } + testTable.insert { it[foo] = 0 } } fail("Should fail on constraint > 0 with $db") } catch (_: SQLException) { // expected } withDb(db) { - assertTrue(TestTable.selectAll().empty()) + assertTrue(testTable.selectAll().empty()) } } finally { withDb(db) { @@ -501,11 +494,12 @@ class InsertTests : DatabaseTestsBase() { } } - @Test fun `rollback on constraint exception normal suspended transactions`() { - val TestTable = object : IntIdTable("TestRollback") { + @Test + fun testRollbackOnConstraintExceptionWithSuspendTransactions() { + 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 } ) @@ -514,12 +508,12 @@ class InsertTests : DatabaseTestsBase() { try { try { withDb(db) { - SchemaUtils.create(TestTable) + SchemaUtils.create(testTable) } runBlocking { newSuspendedTransaction(db = db.db) { - TestTable.insert { it[foo] = 1 } - TestTable.insert { it[foo] = 0 } + testTable.insert { it[foo] = 1 } + testTable.insert { it[foo] = 0 } } } fail("Should fail on constraint > 0") @@ -528,7 +522,7 @@ class InsertTests : DatabaseTestsBase() { } withDb(db) { - assertTrue(TestTable.selectAll().empty()) + assertTrue(testTable.selectAll().empty()) } } finally { withDb(db) { @@ -572,6 +566,47 @@ class InsertTests : DatabaseTestsBase() { it[board] = nullableBoardId } } + } + + class BatchInsertOnConflictDoNothing( + table: Table, + ) : BatchInsertStatement(table) { + override fun prepareSQL(transaction: Transaction, prepared: Boolean) = buildString { + val insertStatement = super.prepareSQL(transaction, prepared) + when (val db = currentTestDB) { + in TestDB.mySqlRelatedDB -> { + append("INSERT IGNORE ") + append(insertStatement.substringAfter("INSERT ")) + } + else -> { + append(insertStatement) + val identifier = if (db == TestDB.H2_PSQL) "" else "(id) " + append(" ON CONFLICT ${identifier}DO NOTHING") + } + } + } + } + + @Test + fun testBatchInsertNumberOfInsertedRows() { + val tab = object : Table("tab") { + val id = varchar("id", 10).uniqueIndex() + } + + withTables(excludeSettings = insertIgnoreUnsupportedDB, tab) { + tab.insert { it[id] = "foo" } + val numInserted = BatchInsertOnConflictDoNothing(tab).run { + addBatch() + this[tab.id] = "foo" + + addBatch() + this[tab.id] = "bar" + + execute(this@withTables) + } + + assertEquals(1, numInserted) + } } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt index c459a7c497..552bff9428 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt @@ -1,5 +1,6 @@ package org.jetbrains.exposed.sql.tests.shared.dml +import nl.altindag.log.LogCaptor import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.tests.DatabaseTestsBase @@ -190,7 +191,10 @@ class JoinTests : DatabaseTestsBase() { } } - @Test fun testNoWarningsOnLeftJoinRegression() { + @Test + fun testNoWarningsOnLeftJoinRegression() { + val logCaptor = LogCaptor.forName(exposedLogger.name) + val MainTable = object : Table("maintable") { val id = integer("idCol") } @@ -208,7 +212,9 @@ class JoinTests : DatabaseTestsBase() { .single() .getOrNull(JoinTable.data) - // Assert no logging took place. No idea how to. + // Assert no logging took place + assertTrue(logCaptor.warnLogs.isEmpty()) + assertTrue(logCaptor.errorLogs.isEmpty()) } } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/LikeTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/LikeTests.kt index 3f10aca784..c95c4f81ea 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/LikeTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/LikeTests.kt @@ -10,7 +10,7 @@ import org.junit.Test class LikeTests : DatabaseTestsBase() { - object t : Table("table") { + object t : Table("testTable") { val id = integer("charnum") val char = varchar("thechar", 255) 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/dml/SelectTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/SelectTests.kt index 80c19a2416..ca8ce9a738 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/SelectTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/SelectTests.kt @@ -246,8 +246,8 @@ class SelectTests : DatabaseTestsBase() { @Test fun `test select on nullable reference column`() { - val firstTable = object : IntIdTable("first") {} - val secondTable = object : IntIdTable("second") { + val firstTable = object : IntIdTable("firstTable") {} + val secondTable = object : IntIdTable("secondTable") { val firstOpt = optReference("first", firstTable) } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpdateTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpdateTests.kt index c8e8b56bc5..dccd460766 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpdateTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpdateTests.kt @@ -85,6 +85,7 @@ class UpdateTests : DatabaseTestsBase() { } } } + @Test fun testUpdateWithJoin02() { withCitiesAndUsers(exclude = TestDB.allH2TestDB + TestDB.SQLITE) { cities, users, userData -> diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt index af52e76c46..554f95a621 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/UpsertTests.kt @@ -7,11 +7,13 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.concat import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus +import org.jetbrains.exposed.sql.statements.BatchUpsertStatement import org.jetbrains.exposed.sql.tests.* import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.expectException import org.junit.Test import java.util.* +import kotlin.properties.Delegates // Upsert implementation does not support H2 version 1 // https://youtrack.jetbrains.com/issue/EXPOSED-30/Phase-Out-Support-for-H2-Version-1.x @@ -21,31 +23,24 @@ class UpsertTests : DatabaseTestsBase() { @Test fun testUpsertWithPKConflict() { - val tester = object : Table("tester") { - val id = integer("id").autoIncrement() - val name = varchar("name", 64) - - override val primaryKey = PrimaryKey(id) - } - - withTables(tester) { testDb -> + withTables(AutoIncTable) { testDb -> excludingH2Version1(testDb) { - val id1 = tester.insert { + val id1 = AutoIncTable.insert { it[name] = "A" - } get tester.id + } get AutoIncTable.id - tester.upsert { + AutoIncTable.upsert { if (testDb in upsertViaMergeDB) it[id] = 2 it[name] = "B" } - tester.upsert { + AutoIncTable.upsert { it[id] = id1 it[name] = "C" } - assertEquals(2, tester.selectAll().count()) - val updatedResult = tester.select { tester.id eq id1 }.single() - assertEquals("C", updatedResult[tester.name]) + assertEquals(2, AutoIncTable.selectAll().count()) + val updatedResult = AutoIncTable.select { AutoIncTable.id eq id1 }.single() + assertEquals("C", updatedResult[AutoIncTable.name]) } } } @@ -68,17 +63,17 @@ class UpsertTests : DatabaseTestsBase() { it[name] = "A" } - tester.upsert { // insert because only 1 constraint is equal + tester.upsert { // insert because only 1 constraint is equal it[idA] = 7 it[idB] = insertStmt get tester.idB it[name] = "B" } - tester.upsert { // insert because both constraints differ + tester.upsert { // insert because both constraints differ it[idA] = 99 it[idB] = 99 it[name] = "C" } - tester.upsert { // update because both constraints match + tester.upsert { // update because both constraints match it[idA] = insertStmt get tester.idA it[idB] = insertStmt get tester.idB it[name] = "D" @@ -162,6 +157,32 @@ class UpsertTests : DatabaseTestsBase() { } } + @Test + fun testUpsertWithUUIDKeyConflict() { + val tester = object : Table("tester") { + val id = uuid("id").autoGenerate() + val title = text("title") + + override val primaryKey = PrimaryKey(id) + } + + withTables(tester) { testDb -> + excludingH2Version1(testDb) { + val uuid1 = tester.upsert { + it[title] = "A" + } get tester.id + tester.upsert { + it[id] = uuid1 + it[title] = "B" + } + + val result = tester.selectAll().single() + assertEquals(uuid1, result[tester.id]) + assertEquals("B", result[tester.title]) + } + } + } + @Test fun testUpsertWithNoUniqueConstraints() { val tester = object : Table("tester") { @@ -259,18 +280,18 @@ class UpsertTests : DatabaseTestsBase() { withTables(tester) { testDb -> excludingH2Version1(testDb) { val testWord = "Test" - tester.upsert { // default expression in insert + tester.upsert { // default expression in insert it[word] = testWord } assertEquals("Phrase", tester.selectAll().single()[tester.phrase]) val phraseConcat = concat(" - ", listOf(tester.word, tester.phrase)) - tester.upsert(onUpdate = listOf(tester.phrase to phraseConcat)) { // expression in update + tester.upsert(onUpdate = listOf(tester.phrase to phraseConcat)) { // expression in update it[word] = testWord } assertEquals("$testWord - $defaultPhrase", tester.selectAll().single()[tester.phrase]) - tester.upsert { // provided expression in insert + tester.upsert { // provided expression in insert it[word] = "$testWord 2" it[phrase] = concat(stringLiteral("foo"), stringLiteral("bar")) } @@ -424,6 +445,56 @@ class UpsertTests : DatabaseTestsBase() { } } + @Test + fun testInsertedCountWithBatchUpsert() { + withTables(AutoIncTable) { testDb -> + excludingH2Version1(testDb) { + // SQL Server requires statements to be executed before results can be obtained + val isNotSqlServer = testDb != TestDB.SQLSERVER + val data = listOf(1 to "A", 2 to "B", 3 to "C") + val newDataSize = data.size + var statement: BatchUpsertStatement by Delegates.notNull() + + // all new rows inserted + AutoIncTable.batchUpsert(data, shouldReturnGeneratedValues = isNotSqlServer) { (id, name) -> + statement = this + this[AutoIncTable.id] = id + this[AutoIncTable.name] = name + } + assertEquals(newDataSize, statement.insertedCount) + + // all existing rows set to their current values + val isH2MysqlMode = testDb == TestDB.H2_MYSQL || testDb == TestDB.H2_MARIADB + var expected = if (isH2MysqlMode) 0 else newDataSize + AutoIncTable.batchUpsert(data, shouldReturnGeneratedValues = isNotSqlServer) { (id, name) -> + statement = this + this[AutoIncTable.id] = id + this[AutoIncTable.name] = name + } + assertEquals(expected, statement.insertedCount) + + // all existing rows updated & 1 new row inserted + val updatedData = data.map { it.first to "new${it.second}" } + (4 to "D") + expected = if (testDb in TestDB.mySqlRelatedDB) newDataSize * 2 + 1 else newDataSize + 1 + AutoIncTable.batchUpsert(updatedData, shouldReturnGeneratedValues = isNotSqlServer) { (id, name) -> + statement = this + this[AutoIncTable.id] = id + this[AutoIncTable.name] = name + } + assertEquals(expected, statement.insertedCount) + + assertEquals(updatedData.size.toLong(), AutoIncTable.selectAll().count()) + } + } + } + + private object AutoIncTable : Table("auto_inc_table") { + val id = integer("id").autoIncrement() + val name = varchar("name", 64) + + override val primaryKey = PrimaryKey(id) + } + private object Words : Table("words") { val word = varchar("name", 64).uniqueIndex() val count = integer("count").default(1) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ColumnWithTransformTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ColumnWithTransformTest.kt new file mode 100644 index 0000000000..245c7e1f18 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ColumnWithTransformTest.kt @@ -0,0 +1,84 @@ +package org.jetbrains.exposed.sql.tests.shared.entities + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.junit.Test + +object TransformationsTable : IntIdTable() { + val value = varchar("value", 50) +} + +object NullableTransformationsTable: IntIdTable() { + val value = varchar("nullable", 50).nullable() +} + +class TransformationEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TransformationsTable) + var value by TransformationsTable.value.transform( + toColumn = { "transformed-$it" }, + toReal = { it.replace("transformed-", "") } + ) +} + +class NullableTransformationEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(NullableTransformationsTable) + var value by NullableTransformationsTable.value.transform( + toColumn = { "transformed-$it" }, + toReal = { it?.replace("transformed-", "") } + ) +} + +class ColumnWithTransformTest: DatabaseTestsBase() { + + @Test + fun `set and get value`() { + withTables(TransformationsTable) { + val entity = TransformationEntity.new { + value = "stuff" + } + + assertEquals("stuff", entity.value) + + val row = TransformationsTable.select(Op.TRUE) + .first() + + assertEquals("transformed-stuff", row[TransformationsTable.value]) + } + } + + @Test + fun `set and get nullable value - while present`() { + withTables(NullableTransformationsTable) { + val entity = NullableTransformationEntity.new { + value = "stuff" + } + + assertEquals("stuff", entity.value) + + val row = NullableTransformationsTable.select(Op.TRUE) + .first() + + assertEquals("transformed-stuff", row[NullableTransformationsTable.value]) + } + } + + @Test + fun `set and get nullable value - while absent`() { + withTables(NullableTransformationsTable) { + val entity = NullableTransformationEntity.new {} + + assertEquals(null, entity.value) + + val row = NullableTransformationsTable.select(Op.TRUE) + .first() + + assertEquals(null, row[NullableTransformationsTable.value]) + } + } +} 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 5d7cbd99ef..98a4f7ef46 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 @@ -8,9 +8,11 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.statements.api.ExposedBlob 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.* import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction +import org.jetbrains.exposed.sql.vendors.OracleDialect import org.junit.Test import java.sql.Connection import java.util.* @@ -81,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") @@ -94,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 @@ -111,7 +116,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testBlobField() { + @Test + fun testBlobField() { withTables(EntityTestsData.YTable) { val y1 = EntityTestsData.YEntity.new { x = false @@ -132,7 +138,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testTextFieldOutsideTheTransaction() { + @Test + fun testTextFieldOutsideTheTransaction() { val objectsToVerify = arrayListOf>() withTables(Humans) { testDb -> val y1 = Human.new { @@ -149,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) { @@ -359,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) @@ -375,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 } @@ -431,6 +440,7 @@ class EntityTests : DatabaseTestsBase() { open class Human(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(Humans) + var h by Humans.h } @@ -502,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" @@ -527,7 +538,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun testOrderByOnEntities() { + @Test + fun testOrderByOnEntities() { withTables(Categories) { Categories.deleteAll() val category1 = Category.new { title = "Test1" } @@ -540,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" @@ -578,6 +591,7 @@ class EntityTests : DatabaseTestsBase() { class Parent(id: EntityID) : LongEntity(id) { companion object : LongEntityClass(Parents) + var name by Parents.name } @@ -588,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 { @@ -611,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()) @@ -622,16 +639,19 @@ class EntityTests : DatabaseTestsBase() { } private fun newTransaction(statement: Transaction.() -> T) = - inTopLevelTransaction(TransactionManager.manager.defaultIsolationLevel, 1, false, null, null, statement) + inTopLevelTransaction(TransactionManager.manager.defaultIsolationLevel, false, null, null, statement) - @Test fun sharingEntityBetweenTransactions() { + @Test + fun sharingEntityBetweenTransactions() { withTables(Humans) { val human1 = newTransaction { + repetitionAttempts = 1 Human.new { this.h = "foo" } } newTransaction { + repetitionAttempts = 1 assertEquals(null, Human.testCache(human1.id)) assertEquals("foo", Humans.selectAll().single()[Humans.h]) human1.h = "bar" @@ -640,6 +660,7 @@ class EntityTests : DatabaseTestsBase() { } newTransaction { + repetitionAttempts = 1 assertEquals("bar", Humans.selectAll().single()[Humans.h]) } } @@ -706,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) @@ -717,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 } @@ -750,10 +776,9 @@ class EntityTests : DatabaseTestsBase() { var holidays by Holiday via SchoolHolidays } - @Test fun preloadReferencesOnASizedIterable() { - + @Test + fun preloadReferencesOnASizedIterable() { withTables(Regions, Schools) { - val region1 = Region.new { name = "United Kingdom" } @@ -779,7 +804,7 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { School.all().with(School::region) assertNotNull(School.testCache(school1.id)) assertNotNull(School.testCache(school2.id)) @@ -792,10 +817,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadReferencesOnAnEntity() { - + @Test + fun preloadReferencesOnAnEntity() { withTables(Regions, Schools) { - val region1 = Region.new { name = "United Kingdom" } @@ -807,7 +831,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 School.find { Schools.id eq school1.id }.first().load(School::region) @@ -824,9 +849,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadOptionalReferencesOnASizedIterable() { + @Test + fun preloadOptionalReferencesOnASizedIterable() { withTables(Regions, Schools) { - val region1 = Region.new { name = "United Kingdom" } @@ -839,6 +864,9 @@ class EntityTests : DatabaseTestsBase() { name = "Eton" region = region1 secondaryRegion = region2 + }.apply { + // otherwise Oracle provides school1.id = 0 to testCache(), which returns null + if (currentDialectTest is OracleDialect) flush() } val school2 = School.new { @@ -848,7 +876,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 School.all().with(School::region, School::secondaryRegion) assertNotNull(School.testCache(school1.id)) assertNotNull(School.testCache(school2.id)) @@ -860,10 +889,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadOptionalReferencesOnAnEntity() { - + @Test + fun preloadOptionalReferencesOnAnEntity() { withTables(Regions, Schools) { - val region1 = Region.new { name = "United Kingdom" } @@ -879,7 +907,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 val school2 = School.find { Schools.id eq school1.id }.first().load(School::secondaryRegion) @@ -890,10 +919,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadReferrersOnASizedIterable() { - + @Test + fun preloadReferrersOnASizedIterable() { withTables(Regions, Schools, Students) { - val region1 = Region.new { name = "United Kingdom" } @@ -939,7 +967,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 val cache = TransactionManager.current().entityCache School.all().with(School::students) @@ -951,9 +980,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadReferrersOnAnEntity() { + @Test + fun preloadReferrersOnAnEntity() { withTables(Regions, Schools, Students) { - val region1 = Region.new { name = "United Kingdom" } @@ -980,7 +1009,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 val cache = TransactionManager.current().entityCache School.find { Schools.id eq school1.id }.first().load(School::students) @@ -990,10 +1020,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadOptionalReferrersOnASizedIterable() { - + @Test + fun preloadOptionalReferrersOnASizedIterable() { withTables(Regions, Schools, Students, Detentions) { - val region1 = Region.new { name = "United Kingdom" } @@ -1025,7 +1054,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 School.all().with(School::students, Student::detentions) val cache = TransactionManager.current().entityCache @@ -1038,10 +1068,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadInnerTableLinkOnASizedIterable() { - + @Test + fun preloadInnerTableLinkOnASizedIterable() { withTables(Regions, Schools, Holidays, SchoolHolidays) { - val now = System.currentTimeMillis() val now10 = now + 10 @@ -1088,7 +1117,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 School.all().with(School::holidays) val cache = TransactionManager.current().entityCache @@ -1099,9 +1129,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadInnerTableLinkOnAnEntity() { + @Test + fun preloadInnerTableLinkOnAnEntity() { withTables(Regions, Schools, Holidays, SchoolHolidays) { - val now = System.currentTimeMillis() val now10 = now + 10 @@ -1158,10 +1188,9 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadRelationAtDepth() { - + @Test + fun preloadRelationAtDepth() { withTables(Regions, Schools, Holidays, SchoolHolidays, Students, Notes) { - val region1 = Region.new { name = "United Kingdom" } @@ -1202,8 +1231,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadBackReferrenceOnASizedIterable() { - + @Test + fun preloadBackReferrenceOnASizedIterable() { withTables(Regions, Schools, Students, StudentBios) { val region1 = Region.new { name = "United States" @@ -1236,7 +1265,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 Student.all().with(Student::bio) val cache = TransactionManager.current().entityCache @@ -1246,8 +1276,8 @@ class EntityTests : DatabaseTestsBase() { } } - @Test fun preloadBackReferrenceOnAnEntity() { - + @Test + fun preloadBackReferrenceOnAnEntity() { withTables(Regions, Schools, Students, StudentBios) { val region1 = Region.new { name = "United States" @@ -1280,7 +1310,8 @@ class EntityTests : DatabaseTestsBase() { commit() - inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE, 1) { + inTopLevelTransaction(Connection.TRANSACTION_SERIALIZABLE) { + repetitionAttempts = 1 Student.all().first().load(Student::bio) val cache = TransactionManager.current().entityCache @@ -1289,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" @@ -1320,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 { @@ -1345,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 { @@ -1360,8 +1395,7 @@ class EntityTests : DatabaseTestsBase() { assertEquals("Test Board", board.name) assertTrue( - createBoardCalled, - "Expected createBoardCalled to be called" + createBoardCalled, "Expected createBoardCalled to be called" ) } } @@ -1389,4 +1423,75 @@ class EntityTests : DatabaseTestsBase() { assertEquals(1, count) } } + + object CreditCards : IntIdTable("CreditCards") { + val number = varchar("number", 16) + val spendingLimit = ulong("spendingLimit").databaseGenerated() + } + + class CreditCard(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(CreditCards) + + var number by CreditCards.number + var spendingLimit by CreditCards.spendingLimit + } + + @Test + fun testDatabaseGeneratedValues() { + withTables(excludeSettings = listOf(TestDB.SQLITE), CreditCards) { testDb -> + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + // The value can also be set using a SQL trigger + exec( + """ + CREATE OR REPLACE FUNCTION set_spending_limit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS + $$ + BEGIN + NEW."spendingLimit" := 10000; + RETURN NEW; + END; + $$; + """.trimIndent() + ) + exec( + """ + CREATE TRIGGER set_spending_limit + BEFORE INSERT + ON CreditCards + FOR EACH ROW + EXECUTE PROCEDURE set_spending_limit(); + """.trimIndent() + ) + } + else -> { + // This table is only used to get the statement that adds the DEFAULT value, and use it with exec + val creditCards2 = object : IntIdTable("CreditCards") { + val spendingLimit = ulong("spendingLimit").default(10000uL) + } + val missingStatements = SchemaUtils.addMissingColumnsStatements(creditCards2) + missingStatements.forEach { + exec(it) + } + } + } + + val creditCardId = CreditCards.insertAndGetId { + it[number] = "0000111122223333" + }.value + assertEquals( + 10000uL, + CreditCards.select { CreditCards.id eq creditCardId }.single()[CreditCards.spendingLimit] + ) + + val creditCard = CreditCard.new { + number = "0000111122223333" + }.apply { + flush() + } + assertEquals(10000uL, creditCard.spendingLimit) + } + } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/SelfReferenceTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/SelfReferenceTest.kt index d322f67535..d46fc7796a 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/SelfReferenceTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/SelfReferenceTest.kt @@ -9,7 +9,6 @@ import org.junit.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -@Suppress("unused") class SortByReferenceTest { @Test diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/FunctionsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/FunctionsTests.kt index 6f4c444bc5..4b7c36d136 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/FunctionsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/FunctionsTests.kt @@ -12,12 +12,7 @@ import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.dml.DMLTestsData import org.jetbrains.exposed.sql.tests.shared.dml.withCitiesAndUsers -import org.jetbrains.exposed.sql.vendors.H2Dialect -import org.jetbrains.exposed.sql.vendors.OracleDialect -import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect -import org.jetbrains.exposed.sql.vendors.SQLServerDialect -import org.jetbrains.exposed.sql.vendors.SQLiteDialect -import org.jetbrains.exposed.sql.vendors.h2Mode +import org.jetbrains.exposed.sql.vendors.* import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -356,16 +351,14 @@ class FunctionsTests : DatabaseTestsBase() { @Test fun testLocate03() { withCitiesAndUsers { cities, _, _ -> - val isCaseSensitiveDialect = currentDialectTest is SQLiteDialect || - currentDialectTest is PostgreSQLDialect || - currentDialectTest is H2Dialect + val isNotCaseSensitiveDialect = currentDialectTest is MysqlDialect || currentDialectTest is SQLServerDialect val locate = cities.name.locate("p") val results = cities.slice(locate).selectAll().toList() - assertEquals(if (isCaseSensitiveDialect) 0 else 5, results[0][locate]) // St. Petersburg + assertEquals(if (isNotCaseSensitiveDialect) 5 else 0, results[0][locate]) // St. Petersburg assertEquals(0, results[1][locate]) // Munich - assertEquals(if (isCaseSensitiveDialect) 0 else 1, results[2][locate]) // Prague + assertEquals(if (isNotCaseSensitiveDialect) 1 else 0, results[2][locate]) // Prague } } 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..46ba5cf2e9 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,30 +67,53 @@ 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)) + ) } } } @Test fun testRoundFunction() { - withTable { + withTable { testDb -> assertExpressionEqual(BigDecimal(10), RoundFunction(intLiteral(10), 0)) assertExpressionEqual(BigDecimal("10.00"), RoundFunction(intLiteral(10), 2)) assertExpressionEqual(BigDecimal(10), RoundFunction(doubleLiteral(10.455), 0)) assertExpressionEqual(BigDecimal(11), RoundFunction(doubleLiteral(10.555), 0)) - assertExpressionEqual(BigDecimal("10.56"), RoundFunction(doubleLiteral(10.555), 2)) + if (testDb == TestDB.SQLITE) { + // Change this when this issue is resolved https://www.sqlite.org/forum/forumpost/2801f84063 + assertExpressionEqual(BigDecimal("10.55"), RoundFunction(doubleLiteral(10.555), 2)) + } else { + assertExpressionEqual(BigDecimal("10.56"), RoundFunction(doubleLiteral(10.555), 2)) + } } } @@ -105,23 +127,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/shared/functions/StatisticsFunctionTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/StatisticsFunctionTests.kt index 1d9fb73113..5d4ce14547 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/StatisticsFunctionTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/StatisticsFunctionTests.kt @@ -44,6 +44,7 @@ class StatisticsFunctionTests : DatabaseTestsBase() { private object SampleTestTable : Table("sample_table") { val number = integer("number").nullable() } + private val data: List = listOf(4, null, 5, null, 6) private val scale = 4 @@ -63,7 +64,38 @@ class StatisticsFunctionTests : DatabaseTestsBase() { } private fun calculateStandardDeviation(isPopulation: Boolean): BigDecimal { - return calculateVariance(isPopulation).sqrt(MathContext(scale, RoundingMode.HALF_EVEN)) + return calculateVariance(isPopulation).simpleSqrt() + } + + fun BigDecimal.simpleSqrt(): BigDecimal { + if (this < BigDecimal.ZERO) throw ArithmeticException("Square root of negative number") + if (this == BigDecimal.ZERO) return BigDecimal.ZERO + + val TWO = BigDecimal(2) + val EPSILON = BigDecimal(0.1).pow(scale) + + var low = BigDecimal.ZERO + var high = max(BigDecimal.ONE) + var result = (low + high).divide(TWO) + + while (true) { + val square = result.multiply(result) + val diff = square.subtract(this).abs() + if (diff < EPSILON) { + break + } + + if (result.multiply(result) < this) { + low = result + } else { + high = result + } + result = (low + high).divide(TWO) + } + + result = result.round(MathContext(scale, RoundingMode.HALF_EVEN)) + result = result.setScale(scale) + return result } private fun calculateVariance(isPopulation: Boolean): BigDecimal { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/WindowFunctionsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/WindowFunctionsTests.kt new file mode 100644 index 0000000000..0ce39db2b4 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/functions/WindowFunctionsTests.kt @@ -0,0 +1,436 @@ +package org.jetbrains.exposed.sql.tests.shared.functions + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.cumeDist +import org.jetbrains.exposed.sql.SqlExpressionBuilder.denseRank +import org.jetbrains.exposed.sql.SqlExpressionBuilder.firstValue +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lag +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lastValue +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lead +import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus +import org.jetbrains.exposed.sql.SqlExpressionBuilder.nthValue +import org.jetbrains.exposed.sql.SqlExpressionBuilder.ntile +import org.jetbrains.exposed.sql.SqlExpressionBuilder.percentRank +import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus +import org.jetbrains.exposed.sql.SqlExpressionBuilder.rank +import org.jetbrains.exposed.sql.SqlExpressionBuilder.rowNumber +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB.* +import org.jetbrains.exposed.sql.tests.TestDB.Companion.allH2TestDB +import org.jetbrains.exposed.sql.tests.shared.assertEqualLists +import org.jetbrains.exposed.sql.tests.shared.dml.DMLTestsData +import org.jetbrains.exposed.sql.tests.shared.dml.withSales +import org.jetbrains.exposed.sql.vendors.currentDialect +import org.junit.Test +import java.math.BigDecimal +import java.math.RoundingMode + +class WindowFunctionsTests : DatabaseTestsBase() { + + private val supportsCountDistinctAsWindowFunction = allH2TestDB + ORACLE + private val supportsStatisticsAggregateFunctions = values().toList() - listOf(SQLSERVER, SQLITE) + private val supportsNthValueFunction = values().toList() - listOf(SQLSERVER) + private val supportsExpressionsInWindowFunctionArguments = values().toList() - listOf(MYSQL) + private val supportsExpressionsInWindowFrameClause = values().toList() - listOf(MYSQL, SQLSERVER, MARIADB) + private val supportsDefaultValueInLeadLagFunctions = values().toList() - listOf(MARIADB) + private val supportsRangeModeWithOffsetFrameBound = values().toList() - listOf(SQLSERVER) + + @Suppress("LongMethod") + @Test + fun testWindowFunctions() = withSales { testDb, sales -> + if (!isOldMySql()) { + sales.assertWindowFunctionDefinition( + rowNumber().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOf(1, 1, 2, 1, 1, 1, 2) + ) + sales.assertWindowFunctionDefinition( + rank().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOf(1, 1, 2, 1, 1, 1, 2) + ) + sales.assertWindowFunctionDefinition( + denseRank().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOf(1, 1, 2, 1, 1, 1, 2) + ) + sales.assertWindowFunctionDefinition( + percentRank().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("0", "0", "1", "0", "0", "0", "1").filterNotNull() + ) + sales.assertWindowFunctionDefinition( + cumeDist().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("0.5", "1", "1", "0.5", "1", "1", "1").filterNotNull() + ) + sales.assertWindowFunctionDefinition( + ntile(intLiteral(2)).over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOf(1, 1, 2, 1, 1, 1, 2) + ) + sales.assertWindowFunctionDefinition( + sales.amount.lag(intLiteral(1)).over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal(null, null, "550.1", null, null, null, "1620.1") + ) + sales.assertWindowFunctionDefinition( + sales.amount.lead(intLiteral(1)).over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("900.3", null, null, "1870.9", null, null, null) + ) + + if (testDb in supportsDefaultValueInLeadLagFunctions) { + sales.assertWindowFunctionDefinition( + sales.amount.lag(intLiteral(1), decimalLiteral(BigDecimal("-1.0"))).over() + .partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("-1", "-1", "550.1", "-1", "-1", "-1", "1620.1") + ) + sales.assertWindowFunctionDefinition( + sales.amount.lead(intLiteral(1), decimalLiteral(BigDecimal("-1.0"))).over() + .partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("900.3", "-1", "-1", "1870.9", "-1", "-1", "-1") + ) + } + + sales.assertWindowFunctionDefinition( + sales.amount.firstValue().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("550.1", "1500.25", "550.1", "1620.1", "650.7", "10.2", "1620.1").filterNotNull() + ) + sales.assertWindowFunctionDefinition( + sales.amount.lastValue().over().partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9").filterNotNull() + ) + + if (testDb in supportsNthValueFunction) { + sales.assertWindowFunctionDefinition( + sales.amount.nthValue(intLiteral(2)).over().partitionBy(sales.year, sales.product) + .orderBy(sales.amount), + listOfBigDecimal(null, null, "900.3", null, null, null, "1870.9") + ) + } + + if (testDb in supportsExpressionsInWindowFunctionArguments) { + sales.assertWindowFunctionDefinition( + ntile(intLiteral(1) + intLiteral(1)).over().partitionBy(sales.year, sales.product) + .orderBy(sales.amount), + listOf(1, 1, 2, 1, 1, 1, 2) + ) + sales.assertWindowFunctionDefinition( + sales.amount.lag(intLiteral(2) - intLiteral(1)).over().partitionBy(sales.year, sales.product) + .orderBy(sales.amount), + listOfBigDecimal(null, null, "550.1", null, null, null, "1620.1") + ) + sales.assertWindowFunctionDefinition( + sales.amount.lead(intLiteral(2) - intLiteral(1)).over().partitionBy(sales.year, sales.product) + .orderBy(sales.amount), + listOfBigDecimal("900.3", null, null, "1870.9", null, null, null) + ) + + if (testDb in supportsNthValueFunction) { + sales.assertWindowFunctionDefinition( + sales.amount.nthValue(intLiteral(1) + intLiteral(1)).over() + .partitionBy(sales.year, sales.product).orderBy(sales.amount), + listOfBigDecimal(null, null, "900.3", null, null, null, "1870.9") + ) + } + } + } + } + + @Suppress("LongMethod") + @Test + fun testAggregateFunctionsAsWindowFunctions() = withSales { testDb, sales -> + if (!isOldMySql()) { + sales.assertWindowFunctionDefinition( + sales.amount.min().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("550.1", "1500.25", "550.1", "1620.1", "650.7", "10.2", "1620.1") + ) + sales.assertWindowFunctionDefinition( + sales.amount.max().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("900.3", "1500.25", "900.3", "1870.9", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sales.amount.avg().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("725.2", "1500.25", "725.2", "1745.5", "650.7", "10.2", "1745.5") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("1450.4", "1500.25", "1450.4", "3491", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sales.amount.count().over().partitionBy(sales.year, sales.product), + listOf(2, 1, 2, 2, 1, 1, 2) + ) + + if (testDb in supportsStatisticsAggregateFunctions) { + sales.assertWindowFunctionDefinition( + sales.amount.stdDevPop().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("175.1", "0", "175.1", "125.4", "0", "0", "125.4") + ) + sales.assertWindowFunctionDefinition( + sales.amount.stdDevSamp().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("247.63", null, "247.63", "177.34", null, null, "177.34") + ) + sales.assertWindowFunctionDefinition( + sales.amount.varPop().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("30660.01", "0", "30660.01", "15725.16", "0", "0", "15725.16") + ) + sales.assertWindowFunctionDefinition( + sales.amount.varSamp().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("61320.02", null, "61320.02", "31450.32", null, null, "31450.32") + ) + } + + if (testDb in supportsCountDistinctAsWindowFunction) { + sales.assertWindowFunctionDefinition( + sales.amount.countDistinct().over().partitionBy(sales.year, sales.product), + listOf(2, 1, 2, 2, 1, 1, 2) + ) + } + } + } + + @Test + fun testPartitionByClause() = withSales { _, sales -> + if (!isOldMySql()) { + sales.assertWindowFunctionDefinition( + sales.amount.sum().over(), + listOfBigDecimal("7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over().partitionBy(), + listOfBigDecimal("7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over().partitionBy(sales.year), + listOfBigDecimal("2950.65", "2950.65", "2950.65", "4151.9", "4151.9", "4151.9", "4151.9") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over().partitionBy(sales.year, sales.product), + listOfBigDecimal("1450.4", "1500.25", "1450.4", "3491", "650.7", "10.2", "3491") + ) + } + } + + @Test + fun testOrderByClause() = withSales { _, sales -> + if (!isOldMySql()) { + sales.assertWindowFunctionDefinition( + sales.amount.sum().over(), + listOfBigDecimal("7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over().orderBy(), + listOfBigDecimal("7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55", "7102.55") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over().orderBy(sales.year), + listOfBigDecimal("2950.65", "2950.65", "2950.65", "7102.55", "7102.55", "7102.55", "7102.55") + ) + sales.assertWindowFunctionDefinition( + sales.amount.sum().over() + .orderBy(sales.year to SortOrder.DESC, sales.product to SortOrder.ASC_NULLS_FIRST), + listOfBigDecimal("7102.55", "5652.15", "7102.55", "3501.2", "4151.9", "10.2", "3501.2") + ) + } + } + + @Suppress("LongMethod") + @Test + fun testWindowFrameClause() = withSales { testDb, sales -> + if (!isOldMySql()) { + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).rows(WindowFrameBound.unboundedPreceding()), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).rows(WindowFrameBound.offsetPreceding(1)), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).rows(WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.offsetPreceding(1), WindowFrameBound.offsetFollowing(1)), + listOfBigDecimal("1450.4", "1500.25", "1450.4", "3491", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.offsetFollowing(1), WindowFrameBound.offsetFollowing(2)), + listOfBigDecimal("900.3", null, null, "1870.9", null, null, null) + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.offsetPreceding(2), WindowFrameBound.offsetPreceding(1)), + listOfBigDecimal(null, null, "550.1", null, null, null, "1620.1") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.offsetPreceding(2), WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.currentRow(), WindowFrameBound.offsetFollowing(2)), + listOfBigDecimal("1450.4", "1500.25", "900.3", "3491", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.currentRow(), WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + + if (testDb in supportsExpressionsInWindowFrameClause) { + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).rows( + WindowFrameBound.offsetPreceding(intLiteral(1) + intLiteral(1)), + WindowFrameBound.offsetFollowing(intLiteral(1) + intLiteral(1)) + ), + listOfBigDecimal("1450.4", "1500.25", "1450.4", "3491", "650.7", "10.2", "3491") + ) + } + + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .rows(WindowFrameBound.currentRow(), WindowFrameBound.unboundedFollowing()), + listOfBigDecimal("1450.4", "1500.25", "900.3", "3491", "650.7", "10.2", "1870.9") + ) + + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).range(WindowFrameBound.unboundedPreceding()), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).range(WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + + if (testDb in supportsRangeModeWithOffsetFrameBound) { + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).range(WindowFrameBound.offsetPreceding(1)), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.offsetPreceding(1), WindowFrameBound.offsetFollowing(1)), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.offsetFollowing(1), WindowFrameBound.offsetFollowing(2)), + listOfBigDecimal(null, null, null, null, null, null, null) + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.offsetPreceding(2), WindowFrameBound.offsetPreceding(1)), + listOfBigDecimal(null, null, null, null, null, null, null) + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.offsetPreceding(2), WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.currentRow(), WindowFrameBound.offsetFollowing(2)), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + + if (testDb in supportsExpressionsInWindowFrameClause) { + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).range( + WindowFrameBound.offsetPreceding(intLiteral(1) + intLiteral(1)), + WindowFrameBound.offsetFollowing(intLiteral(1) + intLiteral(1)) + ), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + } + } + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.currentRow(), WindowFrameBound.unboundedFollowing()), + listOfBigDecimal("1450.4", "1500.25", "900.3", "3491", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .range(WindowFrameBound.currentRow(), WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + + if (currentDialect.supportsWindowFrameGroupsMode) { + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).groups(WindowFrameBound.unboundedPreceding()), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).groups(WindowFrameBound.offsetPreceding(1)), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).groups(WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.offsetPreceding(1), WindowFrameBound.offsetFollowing(1)), + listOfBigDecimal("1450.4", "1500.25", "1450.4", "3491", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.offsetFollowing(1), WindowFrameBound.offsetFollowing(2)), + listOfBigDecimal("900.3", null, null, "1870.9", null, null, null) + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.offsetPreceding(2), WindowFrameBound.offsetPreceding(1)), + listOfBigDecimal(null, null, "550.1", null, null, null, "1620.1") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.offsetPreceding(2), WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "1450.4", "1620.1", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.currentRow(), WindowFrameBound.offsetFollowing(2)), + listOfBigDecimal("1450.4", "1500.25", "900.3", "3491", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.currentRow(), WindowFrameBound.currentRow()), + listOfBigDecimal("550.1", "1500.25", "900.3", "1620.1", "650.7", "10.2", "1870.9") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales).groups( + WindowFrameBound.offsetPreceding(intLiteral(1) + intLiteral(1)), + WindowFrameBound.offsetFollowing(intLiteral(1) + intLiteral(1)) + ), + listOfBigDecimal("1450.4", "1500.25", "1450.4", "3491", "650.7", "10.2", "3491") + ) + sales.assertWindowFunctionDefinition( + sumAmountPartitionByYearProductOrderByAmount(sales) + .groups(WindowFrameBound.currentRow(), WindowFrameBound.unboundedFollowing()), + listOfBigDecimal("1450.4", "1500.25", "900.3", "3491", "650.7", "10.2", "1870.9") + ) + } + } + } + + private fun DMLTestsData.Sales.assertWindowFunctionDefinition( + definition: WindowFunctionDefinition, + expectedResult: List + ) { + val result = slice(definition) + .selectAll() + .orderBy( + year to SortOrder.ASC, + month to SortOrder.ASC, + product to SortOrder.ASC_NULLS_FIRST + ) + .map { it[definition] } + + assertEqualLists(result, expectedResult) + } + + private fun sumAmountPartitionByYearProductOrderByAmount(sales: DMLTestsData.Sales) = + sales.amount.sum().over().partitionBy(sales.year, sales.product).orderBy(sales.amount) + + private fun listOfBigDecimal(vararg numbers: String?): List { + return numbers.map { it?.let { BigDecimal(it).setScale(2, RoundingMode.HALF_UP) } } + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/BooleanColumnTypeTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/BooleanColumnTypeTests.kt index f17c38c7a0..f3f5677cf2 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/BooleanColumnTypeTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/BooleanColumnTypeTests.kt @@ -8,7 +8,7 @@ import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.junit.Test class BooleanColumnTypeTests : DatabaseTestsBase() { - object BooleanTable : IntIdTable() { + object BooleanTable : IntIdTable("booleanTable") { val boolColumn = bool("boolColumn") } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/CharColumnType.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/CharColumnType.kt index be0ca5425a..2f8b29a1d5 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/CharColumnType.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/CharColumnType.kt @@ -8,7 +8,7 @@ import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.junit.Test class CharColumnType : DatabaseTestsBase() { - object CharTable : IntIdTable() { + object CharTable : IntIdTable("charTable") { val charColumn = char("charColumn") } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt new file mode 100644 index 0000000000..c966ac3f0e --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/types/UnsignedColumnTypeTests.kt @@ -0,0 +1,333 @@ +package org.jetbrains.exposed.sql.tests.shared.types + +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.assertEqualCollections +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertFailAndRollback +import org.jetbrains.exposed.sql.tests.shared.assertTrue +import org.jetbrains.exposed.sql.vendors.MysqlDialect +import org.jetbrains.exposed.sql.vendors.SQLServerDialect +import org.junit.Test + +class UnsignedColumnTypeTests : DatabaseTestsBase() { + object UByteTable : Table("ubyte_table") { + val unsignedByte = ubyte("ubyte") + } + + object UShortTable : Table("ushort_table") { + val unsignedShort = ushort("ushort") + } + + object UIntTable : Table("uint_table") { + val unsignedInt = uinteger("uint") + } + + object ULongTable : Table("ulong_table") { + val unsignedLong = ulong("ulong") + } + + @Test + fun testUByteColumnType() { + withTables(UByteTable) { + UByteTable.insert { + it[unsignedByte] = 123u + } + + val result = UByteTable.selectAll().toList() + assertEquals(1, result.size) + assertEquals(123u, result.single()[UByteTable.unsignedByte]) + } + } + + @Test + fun testUByteWithCheckConstraint() { + withTables(UByteTable) { + val ddlEnding = when (currentDialectTest) { + is MysqlDialect -> "(ubyte TINYINT UNSIGNED NOT NULL)" + is SQLServerDialect -> "(ubyte TINYINT NOT NULL)" + else -> "CHECK (ubyte BETWEEN 0 and ${UByte.MAX_VALUE}))" + } + assertTrue(UByteTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + val number = 191.toUByte() + assertTrue(number in Byte.MAX_VALUE.toUByte()..UByte.MAX_VALUE) + + UByteTable.insert { it[unsignedByte] = number } + + val result = UByteTable.selectAll() + assertEquals(number, result.single()[UByteTable.unsignedByte]) + + // test that column itself blocks same out-of-range value that compiler blocks + assertFailAndRollback("Check constraint violation (or out-of-range error in MySQL/MariaDB/SQL Server)") { + val tableName = UByteTable.nameInDatabaseCase() + val columnName = UByteTable.unsignedByte.nameInDatabaseCase() + val outOfRangeValue = UByte.MAX_VALUE + 1u + exec("""INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)""") + } + } + } + + @Test + fun testPreviousUByteColumnTypeWorksWithNewSmallIntType() { + // MySQL and MariaDB type hasn't changed, and PostgreSQL and Oracle never supported TINYINT + withDb(TestDB.allH2TestDB - TestDB.H2_PSQL + TestDB.SQLITE) { testDb -> + try { + val tableName = UByteTable.nameInDatabaseCase() + val columnName = UByteTable.unsignedByte.nameInDatabaseCase() + // create table using previous column type TINYINT + exec("""CREATE TABLE ${addIfNotExistsIfSupported()}$tableName ($columnName TINYINT NOT NULL)""") + + val number1 = Byte.MAX_VALUE.toUByte() + UByteTable.insert { it[unsignedByte] = number1 } + + val result1 = UByteTable.select { UByteTable.unsignedByte eq number1 }.count() + assertEquals(1, result1) + + // TINYINT maps to INTEGER in SQLite, so it will not throw OoR error + if (testDb != TestDB.SQLITE) { + val number2 = (Byte.MAX_VALUE + 1).toUByte() + assertFailAndRollback("Out-of-range (OoR) error") { + UByteTable.insert { it[unsignedByte] = number2 } + assertEquals(0, UByteTable.select { UByteTable.unsignedByte less 0u }.count()) + } + + // modify column to now have SMALLINT type + exec(UByteTable.unsignedByte.modifyStatement().first()) + UByteTable.insert { it[unsignedByte] = number2 } + + val result2 = UByteTable.selectAll().map { it[UByteTable.unsignedByte] } + assertEqualCollections(listOf(number1, number2), result2) + } + } finally { + SchemaUtils.drop(UByteTable) + } + } + } + + @Test + fun testUShortColumnType() { + withTables(UShortTable) { + UShortTable.insert { + it[unsignedShort] = 123u + } + + val result = UShortTable.selectAll().toList() + assertEquals(1, result.size) + assertEquals(123u, result.single()[UShortTable.unsignedShort]) + } + } + + @Test + fun testUShortWithCheckConstraint() { + withTables(UShortTable) { + val ddlEnding = if (currentDialectTest is MysqlDialect) { + "(ushort SMALLINT UNSIGNED NOT NULL)" + } else { + "CHECK (ushort BETWEEN 0 and ${UShort.MAX_VALUE}))" + } + assertTrue(UShortTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + val number = 49151.toUShort() + assertTrue(number in Short.MAX_VALUE.toUShort()..UShort.MAX_VALUE) + + UShortTable.insert { it[unsignedShort] = number } + + val result = UShortTable.selectAll() + assertEquals(number, result.single()[UShortTable.unsignedShort]) + + // test that column itself blocks same out-of-range value that compiler blocks + assertFailAndRollback("Check constraint violation (or out-of-range error in MySQL/MariaDB)") { + val tableName = UShortTable.nameInDatabaseCase() + val columnName = UShortTable.unsignedShort.nameInDatabaseCase() + val outOfRangeValue = UShort.MAX_VALUE + 1u + exec("""INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)""") + } + } + } + + @Test + fun testPreviousUShortColumnTypeWorksWithNewIntType() { + withDb(excludeSettings = listOf(TestDB.MYSQL, TestDB.MARIADB)) { testDb -> + try { + val tableName = UShortTable.nameInDatabaseCase() + val columnName = UShortTable.unsignedShort.nameInDatabaseCase() + // create table using previous column type SMALLINT + exec("""CREATE TABLE ${addIfNotExistsIfSupported()}$tableName ($columnName SMALLINT NOT NULL)""") + + val number1 = Short.MAX_VALUE.toUShort() + UShortTable.insert { it[unsignedShort] = number1 } + + val result1 = UShortTable.select { UShortTable.unsignedShort eq number1 }.count() + assertEquals(1, result1) + + // SMALLINT maps to INTEGER in SQLite and NUMBER(38) in Oracle, so they will not throw OoR error + if (testDb != TestDB.SQLITE && testDb != TestDB.ORACLE) { + val number2 = (Short.MAX_VALUE + 1).toUShort() + assertFailAndRollback("Out-of-range (OoR) error") { + UShortTable.insert { it[unsignedShort] = number2 } + assertEquals(0, UShortTable.select { UShortTable.unsignedShort less 0u }.count()) + } + + // modify column to now have INT type + exec(UShortTable.unsignedShort.modifyStatement().first()) + UShortTable.insert { it[unsignedShort] = number2 } + + val result2 = UShortTable.selectAll().map { it[UShortTable.unsignedShort] } + assertEqualCollections(listOf(number1, number2), result2) + } + } finally { + SchemaUtils.drop(UShortTable) + } + } + } + + @Test + fun testUIntColumnType() { + withTables(UIntTable) { + UIntTable.insert { + it[unsignedInt] = 123u + } + + val result = UIntTable.selectAll().toList() + assertEquals(1, result.size) + assertEquals(123u, result.single()[UIntTable.unsignedInt]) + } + } + + @Test + fun testUIntWithCheckConstraint() { + withTables(UIntTable) { + val ddlEnding = if (currentDialectTest is MysqlDialect) { + "(uint INT UNSIGNED NOT NULL)" + } else { + "CHECK (uint BETWEEN 0 and ${UInt.MAX_VALUE}))" + } + assertTrue(UIntTable.ddl.single().endsWith(ddlEnding, ignoreCase = true)) + + val number = 3_221_225_471u + assertTrue(number in Int.MAX_VALUE.toUInt()..UInt.MAX_VALUE) + + UIntTable.insert { it[unsignedInt] = number } + + val result = UIntTable.selectAll() + assertEquals(number, result.single()[UIntTable.unsignedInt]) + + // test that column itself blocks same out-of-range value that compiler blocks + assertFailAndRollback("Check constraint violation (or out-of-range error in MySQL/MariaDB)") { + val tableName = UIntTable.nameInDatabaseCase() + val columnName = UIntTable.unsignedInt.nameInDatabaseCase() + val outOfRangeValue = UInt.MAX_VALUE.toLong() + 1L + exec("""INSERT INTO $tableName ($columnName) VALUES ($outOfRangeValue)""") + } + } + } + + @Test + fun testPreviousUIntColumnTypeWorksWithNewBigIntType() { + // Oracle was already previously constrained to NUMBER(13) + withDb(excludeSettings = listOf(TestDB.MYSQL, TestDB.MARIADB, TestDB.ORACLE)) { testDb -> + try { + val tableName = UIntTable.nameInDatabaseCase() + val columnName = UIntTable.unsignedInt.nameInDatabaseCase() + // create table using previous column type INT + exec("""CREATE TABLE ${addIfNotExistsIfSupported()}$tableName ($columnName INT NOT NULL)""") + + val number1 = Int.MAX_VALUE.toUInt() + UIntTable.insert { it[unsignedInt] = number1 } + + val result1 = UIntTable.select { UIntTable.unsignedInt eq number1 }.count() + assertEquals(1, result1) + + // INT maps to INTEGER in SQLite, so it will not throw OoR error + if (testDb != TestDB.SQLITE) { + val number2 = Int.MAX_VALUE.toUInt() + 1u + assertFailAndRollback("Out-of-range (OoR) error") { + UIntTable.insert { it[unsignedInt] = number2 } + assertEquals(0, UIntTable.select { UIntTable.unsignedInt less 0u }.count()) + } + + // modify column to now have BIGINT type + exec(UIntTable.unsignedInt.modifyStatement().first()) + UIntTable.insert { it[unsignedInt] = number2 } + + val result2 = UIntTable.selectAll().map { it[UIntTable.unsignedInt] } + assertEqualCollections(listOf(number1, number2), result2) + } + } finally { + SchemaUtils.drop(UIntTable) + } + } + } + + @Test + fun testULongColumnType() { + withTables(ULongTable) { + ULongTable.insert { + it[unsignedLong] = 123uL + } + + val result = ULongTable.selectAll().toList() + assertEquals(1, result.size) + assertEquals(123uL, result.single()[ULongTable.unsignedLong]) + } + } + + @Test + fun testMaxUnsignedTypesInMySql() { + withDb(listOf(TestDB.MYSQL, TestDB.MARIADB)) { + SchemaUtils.create(UByteTable, UShortTable, UIntTable, ULongTable) + + UByteTable.insert { it[unsignedByte] = UByte.MAX_VALUE } + assertEquals(UByte.MAX_VALUE, UByteTable.selectAll().single()[UByteTable.unsignedByte]) + + UShortTable.insert { it[unsignedShort] = UShort.MAX_VALUE } + assertEquals(UShort.MAX_VALUE, UShortTable.selectAll().single()[UShortTable.unsignedShort]) + + UIntTable.insert { it[unsignedInt] = UInt.MAX_VALUE } + assertEquals(UInt.MAX_VALUE, UIntTable.selectAll().single()[UIntTable.unsignedInt]) + + ULongTable.insert { it[unsignedLong] = ULong.MAX_VALUE } + assertEquals(ULong.MAX_VALUE, ULongTable.selectAll().single()[ULongTable.unsignedLong]) + + SchemaUtils.drop(UByteTable, UShortTable, UIntTable, ULongTable) + } + } + + @Test + fun testCheckConstraintNameAcrossMultipleTables() { + val (col1, col2, col3) = listOf("num1", "num2", "num3") + val tester1 = object : Table("tester_1") { + val unsigned1 = ubyte(col1) + val unsigned2 = ushort(col2) + val unsigned3 = uinteger(col3) + } + val tester2 = object : Table("tester_2") { + val unsigned1 = ubyte(col1) + val unsigned2 = ushort(col2) + val unsigned3 = uinteger(col3) + } + + withDb { + try { + SchemaUtils.create(tester1, tester2) + + val (byte, short, integer) = Triple(191.toUByte(), 49151.toUShort(), 3_221_225_471u) + tester1.insert { + it[unsigned1] = byte + it[unsigned2] = short + it[unsigned3] = integer + } + tester2.insert { + it[unsigned1] = byte + it[unsigned2] = short + it[unsigned3] = integer + } + } finally { + SchemaUtils.drop(tester1, tester2) + } + } + } +} 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 new file mode 100644 index 0000000000..0a00115aba --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/sqlite/ForeignKeyConstraintTests.kt @@ -0,0 +1,245 @@ +package org.jetbrains.exposed.sql.tests.sqlite + +import org.jetbrains.exposed.exceptions.ExposedSQLException +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +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.Category +import org.jetbrains.exposed.sql.tests.shared.DEFAULT_CATEGORY_ID +import org.jetbrains.exposed.sql.tests.shared.Item +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.expectException +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.Assume +import org.junit.Test + +class ForeignKeyConstraintTests : DatabaseTestsBase() { + + @Test + fun `test ON DELETE SET DEFAULT for databases that support it without SQLite`() { + withDb(excludeSettings = listOf(TestDB.MARIADB, TestDB.MYSQL, TestDB.SQLITE, TestDB.ORACLE)) { + testOnDeleteSetDefault() + } + } + + @Test + fun `test ON DELETE SET DEFAULT for SQLite`() { + 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() + } + } + + private fun Transaction.testOnDeleteSetDefault() { + SchemaUtils.drop(Category, Item) + SchemaUtils.create(Category, Item) + + Category.insert { + it[id] = DEFAULT_CATEGORY_ID + it[name] = "Default" + } + + val saladsId = 1 + Category.insert { + it[id] = saladsId + it[name] = "Salads" + } + + val tabboulehId = 0 + Item.insert { + it[id] = tabboulehId + it[name] = "Tabbouleh" + it[categoryId] = saladsId + } + + assertEquals( + saladsId, + Item.select { Item.id eq tabboulehId }.single().also { + println("SELECT result = $it") + }[Item.categoryId] + ) + + Category.deleteWhere { id eq saladsId } + + assertEquals( + DEFAULT_CATEGORY_ID, + Item.select { Item.id eq tabboulehId }.single()[Item.categoryId] + ) + } + + @Test + fun `test ON DELETE RESTRICT for databases that support it without SQLite`() { + withDb(excludeSettings = listOf(TestDB.SQLITE, TestDB.SQLSERVER)) { + testRestrict(isTestingOnDelete = true) + } + } + + @Test + fun `test ON DELETE RESTRICT for SQLite`() { + 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" + ) + ) { + testRestrict(isTestingOnDelete = true) + } + } + + @Test + fun `test ON UPDATE RESTRICT for databases that support it without SQLite`() { + withDb(excludeSettings = listOf(TestDB.SQLITE, TestDB.SQLSERVER, TestDB.ORACLE)) { + testRestrict(isTestingOnDelete = false) + } + } + + @Test + fun `test ON UPDATE RESTRICT for SQLite`() { + 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" + ) + ) { + testRestrict(isTestingOnDelete = false) + } + } + + private fun testRestrict(isTestingOnDelete: Boolean) { + val country = object : Table("Country") { + val id = integer("id") + val name = varchar(name = "name", length = 20) + + override val primaryKey = PrimaryKey(id) + } + + val city = object : Table("City") { + val id = integer("id") + val name = varchar(name = "name", length = 20) + val countryId = integer("countryId") + .references( + country.id, + onDelete = if (isTestingOnDelete) ReferenceOption.RESTRICT else null, + onUpdate = if (isTestingOnDelete) null else ReferenceOption.RESTRICT, + ) + + override val primaryKey = PrimaryKey(id) + } + + SchemaUtils.drop(country, city) + SchemaUtils.create(country, city) + + val lebanonId = 0 + country.insert { + it[id] = lebanonId + it[name] = "Lebanon" + } + + val beirutId = 0 + city.insert { + it[id] = beirutId + it[name] = "Beirut" + it[countryId] = 0 + } + + if (isTestingOnDelete) { + expectException { + country.deleteWhere { id eq lebanonId } + } + } else { + expectException { + country.update({ country.id eq lebanonId }) { + it[id] = 1 + } + } + } + } + + @Test + fun testUpdateAndDeleteRulesReadCorrectlyWhenNotSpecifiedInChildTable() { + val category = object : Table("Category") { + val id = integer("id") + + override val primaryKey = PrimaryKey(id) + } + + val item = object : Table("Item") { + val id = integer("id") + val categoryId = integer("categoryId").references(category.id) + + override val primaryKey = PrimaryKey(id) + } + + withTables(category, item) { testDb -> + if (currentDialectTest.supportsOnUpdate) { + val constraints = connection.metadata { + tableConstraints(listOf(item)) + } + constraints.values.forEach { list -> + list.forEach { + when (testDb) { + TestDB.H2_ORACLE, TestDB.H2_SQLSERVER -> { + assertEquals(ReferenceOption.RESTRICT, it.updateRule) + assertEquals(ReferenceOption.RESTRICT, it.deleteRule) + } + else -> { + assertEquals(currentDialectTest.defaultReferenceOption, it.updateRule) + assertEquals(currentDialectTest.defaultReferenceOption, it.deleteRule) + } + } + } + } + } + } + } + + @Test + fun testUpdateAndDeleteRulesReadCorrectlyWhenSpecifiedInChildTable() { + val category = object : Table("Category") { + val id = integer("id") + + override val primaryKey = PrimaryKey(id) + } + + val item = object : Table("Item") { + val id = integer("id") + val categoryId = integer("categoryId") + .references( + category.id, + onUpdate = ReferenceOption.CASCADE, + onDelete = ReferenceOption.CASCADE + ) + + override val primaryKey = PrimaryKey(id) + } + + withTables(category, item) { + if (currentDialectTest.supportsOnUpdate) { + val constraints = connection.metadata { + tableConstraints(listOf(item)) + } + constraints.values.forEach { list -> + list.forEach { + assertEquals(ReferenceOption.CASCADE, it.updateRule) + assertEquals(ReferenceOption.CASCADE, it.deleteRule) + } + } + } + } + } +} 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/exposed-tests/src/test/resources/logback-test.xml b/exposed-tests/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..63b06755b5 --- /dev/null +++ b/exposed-tests/src/test/resources/logback-test.xml @@ -0,0 +1,21 @@ + + + + + %date{HH:mm:ss.SSS} %level %thread %logger{0}:%method:%line - %message %n + + + + + true + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties index fac5aaf4dd..84f9103117 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.41.1 +version=0.44.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb87909..db9a6b825d 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000000..b4c86a21e1 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,6 @@ +# Samples + +This section contains samples for different cases of using Exposed. + +- [exposed-ktor](exposed-ktor): Backend application with CRUD (Create, Read, Update, Delete) endpoins, built using Ktor and Exposed. +- [exposed-spring](exposed-spring): Spring Boot 3 based project with CRUD (Create, Read, Update, Delete) operations. diff --git a/samples/exposed-ktor/.gitignore b/samples/exposed-ktor/.gitignore new file mode 100644 index 0000000000..c426c32f86 --- /dev/null +++ b/samples/exposed-ktor/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/samples/exposed-ktor/README.md b/samples/exposed-ktor/README.md new file mode 100644 index 0000000000..65e5017d48 --- /dev/null +++ b/samples/exposed-ktor/README.md @@ -0,0 +1,16 @@ +# Exposed-Ktor + +This Ktor-based project uses Exposed for CRUD (Create, Read, Update, Delete) operations. Here's how they work: + +- [UsersSchema.kt](src/main/kotlin/plugins/UsersSchema.kt): Describes our database schema. If you need to modify the structure, please take care to + understand the existing design first. +- [Databases.kt](src/main/kotlin/plugins/Databases.kt): Handles CRUD operations with various endpoints. + Backend application with CRUD endpoints built using Ktor and Exposed. + +## Running + +To run the sample, execute the following command in a repository's root directory: + +```bash +./gradlew run +``` diff --git a/samples/exposed-ktor/build.gradle.kts b/samples/exposed-ktor/build.gradle.kts new file mode 100644 index 0000000000..923d7d291d --- /dev/null +++ b/samples/exposed-ktor/build.gradle.kts @@ -0,0 +1,38 @@ +val ktorVersion: String by project +val kotlinVersion: String by project +val logbackVersion: String by project +val exposedVersion: String by project +val h2Version: String by project + +plugins { + 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" +version = "0.0.1" +application { + mainClass.set("io.ktor.server.netty.EngineMain") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion") + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") + implementation("com.h2database:h2:$h2Version") + implementation("io.ktor:ktor-server-netty-jvm:$ktorVersion") + implementation("ch.qos.logback:logback-classic:$logbackVersion") + implementation("io.ktor:ktor-server-config-yaml:$ktorVersion") + testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion") +} diff --git a/samples/exposed-ktor/gradle.properties b/samples/exposed-ktor/gradle.properties new file mode 100644 index 0000000000..bccd2d7a21 --- /dev/null +++ b/samples/exposed-ktor/gradle.properties @@ -0,0 +1,6 @@ +ktorVersion=2.3.4 +kotlinVersion=1.8.10 +logbackVersion=1.2.11 +kotlin.code.style=official +exposedVersion=0.44.0 +h2Version=2.1.214 diff --git a/samples/exposed-ktor/gradle/wrapper/gradle-wrapper.jar b/samples/exposed-ktor/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7454180f2a Binary files /dev/null and b/samples/exposed-ktor/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/exposed-ktor/gradle/wrapper/gradle-wrapper.properties b/samples/exposed-ktor/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ae04661ee7 --- /dev/null +++ b/samples/exposed-ktor/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/exposed-ktor/gradlew b/samples/exposed-ktor/gradlew new file mode 100755 index 0000000000..1b6c787337 --- /dev/null +++ b/samples/exposed-ktor/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/exposed-ktor/gradlew.bat b/samples/exposed-ktor/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/samples/exposed-ktor/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/exposed-ktor/settings.gradle.kts b/samples/exposed-ktor/settings.gradle.kts new file mode 100644 index 0000000000..fca1d9583c --- /dev/null +++ b/samples/exposed-ktor/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "exposed-ktor-crud" diff --git a/samples/exposed-ktor/src/main/kotlin/Application.kt b/samples/exposed-ktor/src/main/kotlin/Application.kt new file mode 100644 index 0000000000..128e0dba5b --- /dev/null +++ b/samples/exposed-ktor/src/main/kotlin/Application.kt @@ -0,0 +1,13 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.ktor + +import io.ktor.server.application.* + +fun main(args: Array): Unit = + io.ktor.server.netty.EngineMain.main(args) + +@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused. +fun Application.module() { + configureDatabases() +} diff --git a/samples/exposed-ktor/src/main/kotlin/plugins/Databases.kt b/samples/exposed-ktor/src/main/kotlin/plugins/Databases.kt new file mode 100644 index 0000000000..76d2243d98 --- /dev/null +++ b/samples/exposed-ktor/src/main/kotlin/plugins/Databases.kt @@ -0,0 +1,51 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.ktor + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.jetbrains.exposed.sql.* + +fun Application.configureDatabases() { + val database = Database.connect( + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", + user = "root", + driver = "org.h2.Driver", + password = "" + ) + val userService = UserService(database) + routing { + // Create user + post("/users") { + val user = call.receive() + val id = userService.create(user) + call.respond(HttpStatusCode.Created, id) + } + // Read user + get("/users/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + val user = userService.read(id) + if (user != null) { + call.respond(HttpStatusCode.OK, user) + } else { + call.respond(HttpStatusCode.NotFound) + } + } + // Update user + put("/users/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + val user = call.receive() + userService.update(id, user) + call.respond(HttpStatusCode.OK) + } + // Delete user + delete("/users/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + userService.delete(id) + call.respond(HttpStatusCode.OK) + } + } +} diff --git a/samples/exposed-ktor/src/main/kotlin/plugins/UsersSchema.kt b/samples/exposed-ktor/src/main/kotlin/plugins/UsersSchema.kt new file mode 100644 index 0000000000..9098c28c90 --- /dev/null +++ b/samples/exposed-ktor/src/main/kotlin/plugins/UsersSchema.kt @@ -0,0 +1,61 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.ktor + +import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.transactions.transaction + +@Serializable +data class ExposedUser(val name: String, val age: Int) +class UserService(private val database: Database) { + object Users : Table() { + val id = integer("id").autoIncrement() + val name = varchar("name", length = 50) + val age = integer("age") + + override val primaryKey = PrimaryKey(id) + } + + init { + transaction(database) { + SchemaUtils.create(Users) + } + } + + suspend fun dbQuery(block: suspend () -> T): T = + newSuspendedTransaction(Dispatchers.IO) { block() } + + suspend fun create(user: ExposedUser): Int = dbQuery { + Users.insert { + it[name] = user.name + it[age] = user.age + }[Users.id] + } + + suspend fun read(id: Int): ExposedUser? { + return dbQuery { + Users.select { Users.id eq id } + .map { ExposedUser(it[Users.name], it[Users.age]) } + .singleOrNull() + } + } + + suspend fun update(id: Int, user: ExposedUser) { + dbQuery { + Users.update({ Users.id eq id }) { + it[name] = user.name + it[age] = user.age + } + } + } + + suspend fun delete(id: Int) { + dbQuery { + Users.deleteWhere { Users.id.eq(id) } + } + } +} diff --git a/samples/exposed-ktor/src/main/resources/application.yaml b/samples/exposed-ktor/src/main/resources/application.yaml new file mode 100644 index 0000000000..838e5fb563 --- /dev/null +++ b/samples/exposed-ktor/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +ktor: + application: + modules: + - org.jetbrains.exposed.samples.ktor.ApplicationKt.module + deployment: + port: 8000 diff --git a/samples/exposed-ktor/src/main/resources/logback.xml b/samples/exposed-ktor/src/main/resources/logback.xml new file mode 100644 index 0000000000..bdbb64ec4b --- /dev/null +++ b/samples/exposed-ktor/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/samples/exposed-spring/.gitignore b/samples/exposed-spring/.gitignore new file mode 100644 index 0000000000..63d2f1a0c7 --- /dev/null +++ b/samples/exposed-spring/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/samples/exposed-spring/README.md b/samples/exposed-spring/README.md new file mode 100644 index 0000000000..deb69a6c4c --- /dev/null +++ b/samples/exposed-spring/README.md @@ -0,0 +1,19 @@ +# Exposed-Spring + +This Spring Boot 3 based project uses Exposed for CRUD (Create, Read, Update, Delete) operations. + +- [UserEntity.kt](src/main/kotlin/domain/UserEntity.kt): Describes our database schema. If you need to modify the structure, please take care to + understand the existing design first. +- [UserService.kt](src/main/kotlin/service/UserService.kt): Handles CRUD operations for user domains. This class determines transaction boundaries via @Transactional, + fetches data via Exposed DSL, and handles Domain objects. +- [UserController.kt](src/main/kotlin/controller/UserController.kt): Defines various endpoints that handles CRUD and calls UserService to process requests. +- [SchemaInitializer.kt](src/main/kotlin/support/SchemaInitialize.kt): Initialize the Database Schema when application is run because the sample project uses h2. +- [SpringApplication.kt](src/main/kotlin/SpringApplication.kt): Define Beans and import Configuration class. Import ExposedAutoConfiguration in this file. + +## Running + +To run the sample, execute the following command in a repository's root directory: + +```bash +./gradlew bootRun +``` diff --git a/samples/exposed-spring/build.gradle.kts b/samples/exposed-spring/build.gradle.kts new file mode 100644 index 0000000000..3a6173c5e0 --- /dev/null +++ b/samples/exposed-spring/build.gradle.kts @@ -0,0 +1,46 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +val exposedVersion: String by project + +plugins { + id("org.springframework.boot") version "3.1.2" + id("io.spring.dependency-management") version "1.1.2" + kotlin("jvm") version "1.8.21" + kotlin("plugin.spring") version "1.8.21" +} + +group = "org.jetbrains.exposed" +version = "1.0.0" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain(17) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.jetbrains.kotlin:kotlin-reflect") + + implementation("com.h2database:h2") + implementation("org.jetbrains.exposed:exposed-spring-boot-starter:$exposedVersion") + + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/samples/exposed-spring/gradle.properties b/samples/exposed-spring/gradle.properties new file mode 100644 index 0000000000..3ad0d3cb13 --- /dev/null +++ b/samples/exposed-spring/gradle.properties @@ -0,0 +1,2 @@ +exposedVersion=0.44.0 +kotlinVersion=1.8.21 diff --git a/samples/exposed-spring/gradle/wrapper/gradle-wrapper.jar b/samples/exposed-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..033e24c4cd Binary files /dev/null and b/samples/exposed-spring/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/exposed-spring/gradle/wrapper/gradle-wrapper.properties b/samples/exposed-spring/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..9f4197d5f4 --- /dev/null +++ b/samples/exposed-spring/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/exposed-spring/gradlew b/samples/exposed-spring/gradlew new file mode 100755 index 0000000000..fcb6fca147 --- /dev/null +++ b/samples/exposed-spring/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/exposed-spring/gradlew.bat b/samples/exposed-spring/gradlew.bat new file mode 100644 index 0000000000..6689b85bee --- /dev/null +++ b/samples/exposed-spring/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/exposed-spring/settings.gradle.kts b/samples/exposed-spring/settings.gradle.kts new file mode 100644 index 0000000000..d46127ec26 --- /dev/null +++ b/samples/exposed-spring/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "exposed-spring" diff --git a/samples/exposed-spring/src/main/kotlin/SpringApplication.kt b/samples/exposed-spring/src/main/kotlin/SpringApplication.kt new file mode 100644 index 0000000000..df3c72ec2a --- /dev/null +++ b/samples/exposed-spring/src/main/kotlin/SpringApplication.kt @@ -0,0 +1,18 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.spring + +import org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration +import org.springframework.boot.autoconfigure.ImportAutoConfiguration +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +// ExposedAutoConfiguration is a Spring Boot auto-configuration class that configures Exposed. +@ImportAutoConfiguration(ExposedAutoConfiguration::class) +class SpringApplication + +fun main(args: Array) { + @Suppress("SpreadOperator") + runApplication(*args) +} diff --git a/samples/exposed-spring/src/main/kotlin/controller/UserController.kt b/samples/exposed-spring/src/main/kotlin/controller/UserController.kt new file mode 100644 index 0000000000..8c6af82bed --- /dev/null +++ b/samples/exposed-spring/src/main/kotlin/controller/UserController.kt @@ -0,0 +1,107 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.spring.controller + +import org.jetbrains.exposed.samples.spring.domain.UserId +import org.jetbrains.exposed.samples.spring.service.UserCreateRequest +import org.jetbrains.exposed.samples.spring.service.UserService +import org.jetbrains.exposed.samples.spring.service.UserUpdateRequest +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1/users") +class UserController( + private val userService: UserService, +) { + // Read User + @GetMapping("/{id}") + fun findUserById( + @PathVariable id: Long + ): ResponseEntity { + val user = userService.findUserById(UserId(id)) + + return if (user != null) { + ResponseEntity.ok( + UserResponse( + id = user.id.value, + name = user.name, + age = user.age, + ) + ) + } else { + ResponseEntity.notFound().build() + } + } + + data class UserResponse( + val id: Long, + val name: String, + val age: Int, + ) + + // Create User + @PostMapping + fun create( + @RequestBody form: UserCreateRequestForm + ): ResponseEntity { + val userId = userService.create( + UserCreateRequest( + name = form.name, + age = form.age, + ) + ) + + return ResponseEntity.ok( + UserCreateResponse( + id = userId.value, + ) + ) + } + + data class UserCreateRequestForm( + val name: String, + val age: Int, + ) + + data class UserCreateResponse(val id: Long) + + // Update User + @PutMapping("/{id}") + fun update( + @PathVariable id: Long, + @RequestBody form: UserUpdateRequestForm + ): ResponseEntity { + userService.update( + id = id, + request = UserUpdateRequest( + name = form.name, + age = form.age, + ) + ) + + return ResponseEntity.ok().build() + } + + data class UserUpdateRequestForm( + val name: String? = null, + val age: Int? = null, + ) + + // Delete User + @DeleteMapping("/{id}") + fun delete( + @PathVariable id: Long + ): ResponseEntity { + userService.delete(UserId(id)) + + return ResponseEntity.noContent().build() + } +} diff --git a/samples/exposed-spring/src/main/kotlin/domain/User.kt b/samples/exposed-spring/src/main/kotlin/domain/User.kt new file mode 100644 index 0000000000..a0ddd712df --- /dev/null +++ b/samples/exposed-spring/src/main/kotlin/domain/User.kt @@ -0,0 +1,12 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.spring.domain + +data class User( + val id: UserId, + val name: String, + val age: Int, +) + +@JvmInline +value class UserId(val value: Long) diff --git a/samples/exposed-spring/src/main/kotlin/domain/UserEntity.kt b/samples/exposed-spring/src/main/kotlin/domain/UserEntity.kt new file mode 100644 index 0000000000..272d2c22b1 --- /dev/null +++ b/samples/exposed-spring/src/main/kotlin/domain/UserEntity.kt @@ -0,0 +1,10 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.spring.domain + +import org.jetbrains.exposed.dao.id.LongIdTable + +object UserEntity : LongIdTable() { + val name = varchar("name", length = 50) + val age = integer("age") +} diff --git a/samples/exposed-spring/src/main/kotlin/service/UserService.kt b/samples/exposed-spring/src/main/kotlin/service/UserService.kt new file mode 100644 index 0000000000..86fe8e4f65 --- /dev/null +++ b/samples/exposed-spring/src/main/kotlin/service/UserService.kt @@ -0,0 +1,64 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.spring.service + +import org.jetbrains.exposed.samples.spring.domain.User +import org.jetbrains.exposed.samples.spring.domain.UserEntity +import org.jetbrains.exposed.samples.spring.domain.UserId +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.update +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +@Transactional +class UserService { + + // read user by user primary key + fun findUserById(id: UserId): User? { + // Use Exposed dsl without `transaction { }` + return UserEntity.select { UserEntity.id eq id.value }.firstOrNull()?.let { + User( + id = UserId(it[UserEntity.id].value), + name = it[UserEntity.name], + age = it[UserEntity.age], + ) + } + } + + // create user + fun create(request: UserCreateRequest): UserId { + val id = UserEntity.insertAndGetId { + it[name] = request.name + it[age] = request.age + } + + return UserId(id.value) + } + + // update user + fun update(id: Long, request: UserUpdateRequest) { + UserEntity.update({ UserEntity.id eq id }) { + request.name?.let { name -> it[UserEntity.name] = name } + request.age?.let { age -> it[UserEntity.age] = age } + } + } + + // delete user + fun delete(id: UserId) { + UserEntity.deleteWhere { UserEntity.id eq id.value } + } +} + +data class UserCreateRequest( + val name: String, + val age: Int, +) + +data class UserUpdateRequest( + val name: String? = null, + val age: Int? = null, +) diff --git a/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt b/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt new file mode 100644 index 0000000000..ed9aa4d17d --- /dev/null +++ b/samples/exposed-spring/src/main/kotlin/support/SchemaInitialize.kt @@ -0,0 +1,19 @@ +@file:Suppress("InvalidPackageDeclaration") + +package org.jetbrains.exposed.samples.spring.support + +import org.jetbrains.exposed.samples.spring.domain.UserEntity +import org.jetbrains.exposed.sql.SchemaUtils +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +@Transactional +class SchemaInitialize : ApplicationRunner { + + override fun run(args: ApplicationArguments?) { + SchemaUtils.create(UserEntity) + } +} diff --git a/samples/exposed-spring/src/main/resources/application.properties b/samples/exposed-spring/src/main/resources/application.properties new file mode 100644 index 0000000000..6945e37dc3 --- /dev/null +++ b/samples/exposed-spring/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password diff --git a/settings.gradle.kts b/settings.gradle.kts index 47d9930671..8b8baaa44f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,9 +11,25 @@ include("exposed-money") include("exposed-bom") include("exposed-kotlin-datetime") include("exposed-crypt") +include("exposed-json") pluginManagement { plugins { - id("org.jetbrains.kotlin.jvm") version "1.7.21" + id("org.jetbrains.kotlin.jvm") version "1.9.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver") version "0.7.0" +} + +toolchainManagement { + jvm { + javaRepositories { + repository("foojay") { + resolverClass.set(org.gradle.toolchains.foojay.FoojayToolchainResolver::class.java) + } + } } } diff --git a/spring-transaction/api/spring-transaction.api b/spring-transaction/api/spring-transaction.api new file mode 100644 index 0000000000..dfecea41c3 --- /dev/null +++ b/spring-transaction/api/spring-transaction.api @@ -0,0 +1,5 @@ +public final class org/jetbrains/exposed/spring/SpringTransactionManager : org/springframework/transaction/support/AbstractPlatformTransactionManager { + public fun (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;Z)V + public synthetic fun (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + diff --git a/spring-transaction/build.gradle.kts b/spring-transaction/build.gradle.kts index e37e551515..a523fa5c25 100644 --- a/spring-transaction/build.gradle.kts +++ b/spring-transaction/build.gradle.kts @@ -1,6 +1,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.exposed.gradle.Versions +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") apply true @@ -10,6 +11,10 @@ repositories { mavenCentral() } +kotlin { + jvmToolchain(17) +} + dependencies { api(project(":exposed-core")) implementation(project(":exposed-jdbc")) @@ -20,7 +25,7 @@ dependencies { testImplementation(project(":exposed-dao")) testImplementation(project(":exposed-tests")) testImplementation(kotlin("test-junit")) - testImplementation("org.jetbrains.kotlinx","kotlinx-coroutines-debug", Versions.kotlinCoroutines) + testImplementation("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", Versions.kotlinCoroutines) testImplementation("org.springframework", "spring-test", Versions.springFramework) testImplementation("org.slf4j", "slf4j-api", Versions.slf4j) testImplementation("org.apache.logging.log4j", "log4j-slf4j-impl", Versions.log4j2) @@ -31,9 +36,16 @@ dependencies { testImplementation("com.h2database", "h2", Versions.h2) } +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "17" + } +} + 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/spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt b/spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt index 952443e45c..05719d4a3a 100644 --- a/spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt +++ b/spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt @@ -6,158 +6,200 @@ import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.exposedLogger -import org.jetbrains.exposed.sql.statements.api.ExposedConnection -import org.jetbrains.exposed.sql.statements.jdbc.JdbcConnectionImpl -import org.jetbrains.exposed.sql.transactions.TransactionInterface import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.springframework.jdbc.datasource.ConnectionHolder -import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.jetbrains.exposed.sql.transactions.transactionManager import org.springframework.transaction.TransactionDefinition import org.springframework.transaction.TransactionSystemException -import org.springframework.transaction.support.DefaultTransactionDefinition +import org.springframework.transaction.support.AbstractPlatformTransactionManager import org.springframework.transaction.support.DefaultTransactionStatus -import org.springframework.transaction.support.TransactionSynchronizationManager +import org.springframework.transaction.support.SmartTransactionObject import javax.sql.DataSource class SpringTransactionManager( dataSource: DataSource, - databaseConfig: DatabaseConfig = DatabaseConfig { }, + databaseConfig: DatabaseConfig = DatabaseConfig {}, private val showSql: Boolean = false, - @Volatile override var defaultReadOnly: Boolean = databaseConfig.defaultReadOnly, - @Volatile override var defaultRepetitionAttempts: Int = databaseConfig.defaultRepetitionAttempts -) : DataSourceTransactionManager(dataSource), TransactionManager { +) : AbstractPlatformTransactionManager() { + + private var _database: Database + + private var _transactionManager: TransactionManager + + private val threadLocalTransactionManager: TransactionManager + get() = _transactionManager init { - this.isRollbackOnCommitFailure = true + _database = Database.connect( + datasource = dataSource, databaseConfig = databaseConfig + ).apply { + _transactionManager = this.transactionManager + } + + isNestedTransactionAllowed = databaseConfig.useNestedTransactions } - private val db = Database.connect( - datasource = dataSource, - databaseConfig = databaseConfig - ) { this } + /** + * ExposedConnection implements savepoint by itself + * `useSavepointForNestedTransaction` is use `SavepointManager` for nested transaction + * + * So we don't need to use java savepoint for nested transaction + */ + override fun useSavepointForNestedTransaction() = false + + override fun doGetTransaction(): Any { + val outerManager = TransactionManager.manager + val outer = threadLocalTransactionManager.currentOrNull() + + return ExposedTransactionObject( + manager = threadLocalTransactionManager, + outerManager = outerManager, + outerTransaction = outer, + ) + } - @Volatile override var defaultIsolationLevel: Int = -1 - get() { - if (field == -1) { - field = Database.getDefaultIsolationLevel(db) - } - return field + override fun doSuspend(transaction: Any): Any { + val trxObject = transaction as ExposedTransactionObject + val currentManager = trxObject.manager + + return SuspendedObject( + transaction = currentManager.currentOrNull() as Transaction, + manager = currentManager, + ).apply { + currentManager.bindTransactionToThread(null) + TransactionManager.resetCurrent(null) } + } + + override fun doResume(transaction: Any?, suspendedResources: Any) { + val suspendedObject = suspendedResources as SuspendedObject - private val springTxKey = "SPRING_TX_KEY" + TransactionManager.resetCurrent(suspendedObject.manager) + threadLocalTransactionManager.bindTransactionToThread(suspendedObject.transaction) + } + + private data class SuspendedObject( + val transaction: Transaction, + val manager: TransactionManager + ) + + override fun isExistingTransaction(transaction: Any): Boolean { + val trxObject = transaction as ExposedTransactionObject + return trxObject.getCurrentTransaction() != null + } override fun doBegin(transaction: Any, definition: TransactionDefinition) { - super.doBegin(transaction, definition) + val trxObject = transaction as ExposedTransactionObject - if (TransactionSynchronizationManager.hasResource(obtainDataSource())) { - currentOrNull() ?: initTransaction() - } - if (!TransactionSynchronizationManager.hasResource(springTxKey)) { - TransactionSynchronizationManager.bindResource(springTxKey, transaction) + val currentTransactionManager = trxObject.manager + TransactionManager.resetCurrent(threadLocalTransactionManager) + + currentTransactionManager.newTransaction( + isolation = definition.isolationLevel, + readOnly = definition.isReadOnly, + outerTransaction = currentTransactionManager.currentOrNull() + ).apply { + if (showSql) { + addLogger(StdOutSqlLogger) + } } } + override fun doCommit(status: DefaultTransactionStatus) { + val trxObject = status.transaction as ExposedTransactionObject + TransactionManager.resetCurrent(trxObject.manager) + trxObject.commit() + } + + override fun doRollback(status: DefaultTransactionStatus) { + val trxObject = status.transaction as ExposedTransactionObject + TransactionManager.resetCurrent(trxObject.manager) + trxObject.rollback() + } + override fun doCleanupAfterCompletion(transaction: Any) { - super.doCleanupAfterCompletion(transaction) - if (!TransactionSynchronizationManager.hasResource(obtainDataSource())) { - TransactionSynchronizationManager.unbindResourceIfPossible(this) - TransactionSynchronizationManager.unbindResource(springTxKey) - } - if (TransactionSynchronizationManager.isSynchronizationActive() && TransactionSynchronizationManager.getSynchronizations().isEmpty()) { - TransactionSynchronizationManager.clearSynchronization() + val trxObject = transaction as ExposedTransactionObject + + trxObject.cleanUpTransactionIfIsPossible { + closeStatementsAndConnections(it) } - TransactionManager.resetCurrent(null) - } - override fun doSuspend(transaction: Any): Any { - TransactionSynchronizationManager.unbindResourceIfPossible(this) - return super.doSuspend(transaction) + trxObject.setCurrentToOuter() } - override fun doCommit(status: DefaultTransactionStatus) { + private fun closeStatementsAndConnections(transaction: Transaction) { + val currentStatement = transaction.currentStatement @Suppress("TooGenericExceptionCaught") try { - currentOrNull()?.commit() - } catch (e: Exception) { - throw TransactionSystemException(e.message.orEmpty(), e) + currentStatement?.let { + it.closeIfPossible() + transaction.currentStatement = null + } + transaction.closeExecutedStatements() + } catch (error: Exception) { + exposedLogger.warn("Statements close failed", error) } - } - override fun doRollback(status: DefaultTransactionStatus) { @Suppress("TooGenericExceptionCaught") try { - currentOrNull()?.rollback() - } catch (e: Exception) { - throw TransactionSystemException(e.message.orEmpty(), e) + transaction.close() + } catch (error: Exception) { + exposedLogger.warn("Transaction close failed: ${error.message}. Statement: $currentStatement", error) } } - override fun newTransaction(isolation: Int, readOnly: Boolean, outerTransaction: Transaction?): Transaction { - val tDefinition = DefaultTransactionDefinition().apply { - isReadOnly = readOnly - isolationLevel = isolation - } + override fun doSetRollbackOnly(status: DefaultTransactionStatus) { + val trxObject = status.transaction as ExposedTransactionObject + trxObject.setRollbackOnly() + } - getTransaction(tDefinition) + private data class ExposedTransactionObject( + val manager: TransactionManager, + val outerManager: TransactionManager, + private val outerTransaction: Transaction?, + ) : SmartTransactionObject { - return currentOrNull() ?: initTransaction() - } + private var isRollback: Boolean = false - private fun initTransaction(): Transaction { - val connection = (TransactionSynchronizationManager.getResource(obtainDataSource()) as ConnectionHolder).connection + fun cleanUpTransactionIfIsPossible(block: (transaction: Transaction) -> Unit) { + val currentTransaction = getCurrentTransaction() + if (currentTransaction != null) { + block(currentTransaction) + } + } - val transactionImpl = try { - SpringTransaction(JdbcConnectionImpl(connection), db, defaultIsolationLevel, defaultReadOnly, currentOrNull()) - } catch (e: Exception) { - exposedLogger.error("Failed to start transaction. Connection will be closed.", e) - connection.close() - throw e + fun setCurrentToOuter() { + manager.bindTransactionToThread(outerTransaction) + TransactionManager.resetCurrent(outerManager) } - TransactionManager.resetCurrent(this) - return Transaction(transactionImpl).apply { - TransactionSynchronizationManager.bindResource(this@SpringTransactionManager, this) - if (showSql) { - addLogger(StdOutSqlLogger) + + @Suppress("TooGenericExceptionCaught") + fun commit() { + try { + manager.currentOrNull()?.commit() + } catch (error: Exception) { + throw TransactionSystemException(error.message.orEmpty(), error) } } - } - override fun currentOrNull(): Transaction? = TransactionSynchronizationManager.getResource(this) as Transaction? - override fun bindTransactionToThread(transaction: Transaction?) { - if (transaction != null) { - bindResourceForSure(this, transaction) - } else { - TransactionSynchronizationManager.unbindResourceIfPossible(this) + @Suppress("TooGenericExceptionCaught") + fun rollback() { + try { + manager.currentOrNull()?.rollback() + } catch (error: Exception) { + throw TransactionSystemException(error.message.orEmpty(), error) + } } - } - - private fun bindResourceForSure(key: Any, value: Any) { - TransactionSynchronizationManager.unbindResourceIfPossible(key) - TransactionSynchronizationManager.bindResource(key, value) - } - private inner class SpringTransaction( - override val connection: ExposedConnection<*>, - override val db: Database, - override val transactionIsolation: Int, - override val readOnly: Boolean, - override val outerTransaction: Transaction? - ) : TransactionInterface { + fun getCurrentTransaction(): Transaction? = manager.currentOrNull() - override fun commit() { - connection.commit() + fun setRollbackOnly() { + isRollback = true } - override fun rollback() { - connection.rollback() - } + override fun isRollbackOnly() = isRollback - override fun close() { - if (TransactionSynchronizationManager.isActualTransactionActive()) { - TransactionSynchronizationManager.getResource(springTxKey)?.let { springTx -> - this@SpringTransactionManager.doCleanupAfterCompletion(springTx) - } - } + override fun flush() { + // Do noting } } } diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ConnectionSpy.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ConnectionSpy.kt new file mode 100644 index 0000000000..120a0a6f7b --- /dev/null +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ConnectionSpy.kt @@ -0,0 +1,89 @@ +package org.jetbrains.exposed.spring + +import java.sql.Connection +import java.sql.Savepoint + +internal class ConnectionSpy(private val connection: Connection) : Connection by connection { + + var commitCallCount: Int = 0 + var rollbackCallCount: Int = 0 + var closeCallCount: Int = 0 + var releaseSavepointCallCount: Int = 0 + var mockReadOnly: Boolean = false + var mockIsClosed: Boolean = false + var mockAutoCommit: Boolean = false + var mockTransactionIsolation: Int = Connection.TRANSACTION_READ_COMMITTED + var mockCommit: () -> Unit = {} + var mockRollback: () -> Unit = {} + private val callOrder = mutableListOf() + + fun verifyCallOrder(vararg functions: String): Boolean { + val indices = functions.map { callOrder.indexOf(it) } + return indices.none { it == -1 } && indices == indices.sorted() + } + + fun clearMock() { + commitCallCount = 0 + rollbackCallCount = 0 + closeCallCount = 0 + releaseSavepointCallCount = 0 + mockAutoCommit = false + mockReadOnly = false + mockIsClosed = false + mockTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED + mockCommit = {} + mockRollback = {} + callOrder.clear() + } + + override fun close() { + callOrder.add("close") + closeCallCount++ + } + + override fun setAutoCommit(autoCommit: Boolean) { + callOrder.add("setAutoCommit") + mockAutoCommit = autoCommit + } + + override fun getAutoCommit() = mockAutoCommit + override fun commit() { + callOrder.add("commit") + commitCallCount++ + mockCommit() + } + + override fun rollback() { + callOrder.add("rollback") + rollbackCallCount++ + mockRollback() + } + + override fun rollback(savepoint: Savepoint?) { + callOrder.add("rollback") + rollbackCallCount++ + mockRollback() + } + + override fun releaseSavepoint(savepoint: Savepoint?) { + callOrder.add("releaseSavepoint") + releaseSavepointCallCount++ + } + + override fun isClosed(): Boolean { + callOrder.add("isClosed") + return mockIsClosed + } + + override fun setReadOnly(readOnly: Boolean) { + callOrder.add("setReadOnly") + mockReadOnly = readOnly + } + + override fun isReadOnly(): Boolean { + callOrder.add("isReadOnly") + return mockReadOnly + } + + override fun getTransactionIsolation() = mockTransactionIsolation +} diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/DataSourceSpy.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/DataSourceSpy.kt new file mode 100644 index 0000000000..2dfbf3e341 --- /dev/null +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/DataSourceSpy.kt @@ -0,0 +1,21 @@ +package org.jetbrains.exposed.spring + +import java.io.PrintWriter +import java.sql.Connection +import java.sql.DriverManager +import java.util.logging.Logger +import javax.sql.DataSource + +internal class DataSourceSpy(connectionSpy: (Connection) -> Connection) : DataSource { + var con: Connection = connectionSpy(DriverManager.getConnection("jdbc:h2:mem:test")) + + override fun getConnection() = con + override fun getLogWriter(): PrintWriter = throw NotImplementedError() + override fun setLogWriter(out: PrintWriter?) = throw NotImplementedError() + override fun setLoginTimeout(seconds: Int) = throw NotImplementedError() + override fun getLoginTimeout(): Int = throw NotImplementedError() + override fun getParentLogger(): Logger = throw NotImplementedError() + override fun unwrap(iface: Class?): T = throw NotImplementedError() + override fun isWrapperFor(iface: Class<*>?): Boolean = throw NotImplementedError() + override fun getConnection(username: String?, password: String?): Connection = throw NotImplementedError() +} diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/EntityUpdateTest.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/EntityUpdateTest.kt index ac33f363ae..697e959e44 100644 --- a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/EntityUpdateTest.kt +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/EntityUpdateTest.kt @@ -14,35 +14,42 @@ import kotlin.test.fail open class EntityUpdateTest : SpringTransactionTestBase() { - object t1 : IntIdTable() { + object T1 : IntIdTable() { val c1 = varchar("c1", Int.MIN_VALUE.toString().length) } - class dao(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(t1) - var c1 by t1.c1 + class DAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(T1) + + var c1 by T1.c1 } - @Test @Transactional @Commit + @Test + @Transactional + @Commit open fun test1() { - SchemaUtils.create(t1) - t1.insert { + SchemaUtils.create(T1) + T1.insert { it[c1] = "new" } - Assert.assertEquals("new", dao.findById(1)?.c1) + Assert.assertEquals("new", DAO.findById(1)?.c1) } - @Test @Transactional @Commit + @Test + @Transactional + @Commit open fun test2() { - val entity = dao.findById(1) ?: fail() + val entity = DAO.findById(1) ?: fail() entity.c1 = "updated" - Assert.assertEquals("updated", dao.findById(1)?.c1) + Assert.assertEquals("updated", DAO.findById(1)?.c1) } - @Test @Transactional @Commit + @Test + @Transactional + @Commit open fun test3() { - val entity = dao.findById(1) ?: fail() + val entity = DAO.findById(1) ?: fail() Assert.assertEquals("updated", entity.c1) - SchemaUtils.drop(t1) + SchemaUtils.drop(T1) } } diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ExposedTransactionManagerTest.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ExposedTransactionManagerTest.kt index acac6328d7..a4acbebaf2 100644 --- a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ExposedTransactionManagerTest.kt +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/ExposedTransactionManagerTest.kt @@ -4,72 +4,300 @@ import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.selectAll -import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Assert import org.junit.Test import org.springframework.test.annotation.Commit import org.springframework.test.annotation.Repeat +import org.springframework.transaction.IllegalTransactionStateException import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.TransactionStatus +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.support.TransactionTemplate import java.util.* -import kotlin.test.assertNull +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertFailsWith open class ExposedTransactionManagerTest : SpringTransactionTestBase() { - object t1 : Table() { + object T1 : Table() { val c1 = varchar("c1", Int.MIN_VALUE.toString().length) } - @Test @Transactional @Commit - @Repeat(5) - open fun testConnection() { - val pm = ctx.getBean(PlatformTransactionManager::class.java) - if (pm !is SpringTransactionManager) error("Wrong txManager instance: ${pm.javaClass.name}") + private fun T1.insertRandom() { + insert { + it[c1] = Random().nextInt().toString() + } + } - SchemaUtils.create(t1) - t1.insert { - it[t1.c1] = "112" + private fun PlatformTransactionManager.execute( + propagationBehavior: Int = TransactionDefinition.PROPAGATION_REQUIRED, + block: (TransactionStatus) -> Unit + ) { + if (this !is SpringTransactionManager) error("Wrong txManager instance: ${this.javaClass.name}") + val tt = TransactionTemplate(this) + tt.propagationBehavior = propagationBehavior + tt.executeWithoutResult { + block(it) } + } - Assert.assertEquals(t1.selectAll().count(), 1) - SchemaUtils.drop(t1) + @BeforeTest + fun beforeTest() { + transactionManager.execute { + SchemaUtils.create(T1) + } + } + + @Test + @Transactional + @Commit + @Repeat(5) + open fun testConnection() { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) } - @Test @Transactional @Commit + @Test + @Transactional + @Commit @Repeat(5) open fun testConnection2() { - SchemaUtils.create(t1) val rnd = Random().nextInt().toString() - t1.insert { - it[t1.c1] = rnd + T1.insert { + it[c1] = rnd } - - Assert.assertEquals(t1.selectAll().single()[t1.c1], rnd) - SchemaUtils.drop(t1) + Assert.assertEquals(rnd, T1.selectAll().single()[T1.c1]) } @Test @Repeat(5) - fun testConnectionWithoutAnnotation() { + @Commit + open fun testConnectionCombineWithExposedTransaction() { transaction { - SchemaUtils.create(t1) val rnd = Random().nextInt().toString() - t1.insert { - it[t1.c1] = rnd + T1.insert { + it[c1] = rnd } + Assert.assertEquals(rnd, T1.selectAll().single()[T1.c1]) - Assert.assertEquals(t1.selectAll().single()[t1.c1], rnd) + transactionManager.execute { + T1.insertRandom() + Assert.assertEquals(2, T1.selectAll().count()) + } } - assertNull(TransactionManager.currentOrNull()) + } + + @Test + @Repeat(5) + @Commit + @Transactional + open fun testConnectionCombineWithExposedTransaction2() { + val rnd = Random().nextInt().toString() + T1.insert { + it[c1] = rnd + } + Assert.assertEquals(rnd, T1.selectAll().single()[T1.c1]) + transaction { - val rnd = Random().nextInt().toString() - t1.insert { - it[t1.c1] = rnd + T1.insertRandom() + Assert.assertEquals(2, T1.selectAll().count()) + } + } + + /** + * Test For Propagation.NESTED + * Execute within a nested transaction if a current transaction exists, behave like REQUIRED otherwise. + */ + @Test + @Repeat(5) + @Transactional + open fun testConnectionWithNestedTransactionCommit() { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + transactionManager.execute(TransactionDefinition.PROPAGATION_NESTED) { + T1.insertRandom() + Assert.assertEquals(2, T1.selectAll().count()) + } + Assert.assertEquals(2, T1.selectAll().count()) + } + + /** + * Test For Propagation.NESTED with inner roll-back + * The nested transaction will be roll-back only inner transaction when the transaction marks as rollback. + */ + @Test + @Repeat(5) + @Transactional + open fun testConnectionWithNestedTransactionInnerRollback() { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + transactionManager.execute(TransactionDefinition.PROPAGATION_NESTED) { status -> + T1.insertRandom() + Assert.assertEquals(2, T1.selectAll().count()) + status.setRollbackOnly() + } + Assert.assertEquals(1, T1.selectAll().count()) + } + + /** + * Test For Propagation.NESTED with outer roll-back + * The nested transaction will be roll-back entire transaction when the transaction marks as rollback. + */ + @Test + @Repeat(5) + fun testConnectionWithNestedTransactionOuterRollback() { + transactionManager.execute { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + it.setRollbackOnly() + + transactionManager.execute(TransactionDefinition.PROPAGATION_NESTED) { + T1.insertRandom() + Assert.assertEquals(2, T1.selectAll().count()) + } + Assert.assertEquals(2, T1.selectAll().count()) + } + + transactionManager.execute { + Assert.assertEquals(0, T1.selectAll().count()) + } + } + + /** + * Test For Propagation.REQUIRES_NEW + * Create a new transaction, and suspend the current transaction if one exists. + */ + @Test + @Repeat(5) + @Transactional + open fun testConnectionWithRequiresNew() { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + transactionManager.execute(TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + Assert.assertEquals(0, T1.selectAll().count()) + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + } + Assert.assertEquals(2, T1.selectAll().count()) + } + + /** + * Test For Propagation.REQUIRES_NEW with inner transaction roll-back + * The inner transaction will be roll-back only inner transaction when the transaction marks as rollback. + * And since isolation level is READ_COMMITTED, the inner transaction can't see the changes of outer transaction. + */ + @Test + @Repeat(5) + fun testConnectionWithRequiresNewWithInnerTransactionRollback() { + transactionManager.execute { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + transactionManager.execute(TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + T1.insertRandom() + Assert.assertEquals(1, T1.selectAll().count()) + it.setRollbackOnly() + } + Assert.assertEquals(1, T1.selectAll().count()) + } + + transactionManager.execute { + Assert.assertEquals(1, T1.selectAll().count()) + } + } + + /** + * Test For Propagation.NEVER + * Execute non-transactionally, throw an exception if a transaction exists. + */ + @Test + @Repeat(5) + @Transactional(propagation = Propagation.NEVER) + open fun testPropagationNever() { + assertFailsWith { // Should Be "No transaction exist" + T1.insertRandom() + } + } + + /** + * Test For Propagation.NEVER + * Throw an exception cause outer transaction exists. + */ + @Test + @Repeat(5) + @Transactional + open fun testPropagationNeverWithExistingTransaction() { + assertFailsWith { + T1.insertRandom() + transactionManager.execute(TransactionDefinition.PROPAGATION_NEVER) { + T1.insertRandom() + } + } + } + + /** + * Test For Propagation.MANDATORY + * Support a current transaction, throw an exception if none exists. + */ + @Test + @Repeat(5) + @Transactional + open fun testPropagationMandatoryWithTransaction() { + T1.insertRandom() + transactionManager.execute(TransactionDefinition.PROPAGATION_MANDATORY) { + T1.insertRandom() + } + } + + /** + * Test For Propagation.MANDATORY + * Throw an exception cause no transaction exists. + */ + @Test + @Repeat(5) + open fun testPropagationMandatoryWithoutTransaction() { + assertFailsWith { + transactionManager.execute(TransactionDefinition.PROPAGATION_MANDATORY) { + T1.insertRandom() + } + } + } + + /** + * Test For Propagation.SUPPORTS + * Support a current transaction, execute non-transactionally if none exists. + */ + @Test + @Repeat(5) + @Transactional + open fun testPropagationSupportWithTransaction() { + T1.insertRandom() + transactionManager.execute(TransactionDefinition.PROPAGATION_SUPPORTS) { + T1.insertRandom() + } + } + + /** + * Test For Propagation.SUPPORTS + * Execute non-transactionally if none exists. + */ + @Test + @Repeat(5) + open fun testPropagationSupportWithoutTransaction() { + transactionManager.execute(TransactionDefinition.PROPAGATION_SUPPORTS) { + assertFailsWith { // Should Be "No transaction exist" + T1.insertRandom() } + } + } - Assert.assertEquals(2, t1.selectAll().count()) - SchemaUtils.drop(t1) + @AfterTest + fun afterTest() { + transactionManager.execute { + SchemaUtils.drop(T1) } } } diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringCoroutineTest.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringCoroutineTest.kt index 03d3eb9561..25bdee017f 100644 --- a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringCoroutineTest.kt +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringCoroutineTest.kt @@ -25,14 +25,15 @@ open class SpringCoroutineTest : SpringTransactionTestBase() { } @RepeatableTest(times = 5) - @Test @Transactional @Commit + @Test + @Transactional + @Commit // Is this test flaky? open fun testNestedCoroutineTransaction() { try { SchemaUtils.create(Testing) val mainJob = GlobalScope.async { - val results = (1..5).map { indx -> suspendedTransactionAsync(Dispatchers.IO) { Testing.insert { } diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt new file mode 100644 index 0000000000..3ac2fe4f0d --- /dev/null +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt @@ -0,0 +1,230 @@ +package org.jetbrains.exposed.spring + +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.deleteAll +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.Assert +import org.junit.Test +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.springframework.transaction.annotation.EnableTransactionManagement +import org.springframework.transaction.annotation.Transactional +import javax.sql.DataSource +import kotlin.test.BeforeTest + +open class SpringMultiContainerTransactionTest { + + val orderContainer = AnnotationConfigApplicationContext(OrderConfig::class.java) + val paymentContainer = AnnotationConfigApplicationContext(PaymentConfig::class.java) + + val orders: Orders = orderContainer.getBean(Orders::class.java) + val payments: Payments = paymentContainer.getBean(Payments::class.java) + + @BeforeTest + open fun beforeTest() { + orders.init() + payments.init() + } + + @Test + open fun test1() { + Assert.assertEquals(0, orders.findAll().size) + Assert.assertEquals(0, payments.findAll().size) + } + + @Test + open fun test2() { + orders.create() + Assert.assertEquals(1, orders.findAll().size) + payments.create() + Assert.assertEquals(1, payments.findAll().size) + } + + @Test + open fun test3() { + orders.transaction { + payments.create() + orders.create() + payments.create() + } + Assert.assertEquals(1, orders.findAll().size) + Assert.assertEquals(2, payments.findAll().size) + } + + @Test + open fun test4() { + kotlin.runCatching { + orders.transaction { + orders.create() + payments.create() + throw SpringTransactionTestException() + } + } + Assert.assertEquals(0, orders.findAll().size) + Assert.assertEquals(1, payments.findAll().size) + } + + @Test + open fun test5() { + kotlin.runCatching { + orders.transaction { + orders.create() + payments.databaseTemplate { + payments.create() + throw SpringTransactionTestException() + } + } + } + Assert.assertEquals(0, orders.findAll().size) + Assert.assertEquals(0, payments.findAll().size) + } + + @Test + open fun test6() { + Assert.assertEquals(0, orders.findAllWithExposedTrxBlock().size) + Assert.assertEquals(0, payments.findAllWithExposedTrxBlock().size) + } + + @Test + open fun test7() { + orders.createWithExposedTrxBlock() + Assert.assertEquals(1, orders.findAllWithExposedTrxBlock().size) + payments.createWithExposedTrxBlock() + Assert.assertEquals(1, payments.findAllWithExposedTrxBlock().size) + } + + @Test + open fun test8() { + orders.transaction { + payments.createWithExposedTrxBlock() + orders.createWithExposedTrxBlock() + payments.createWithExposedTrxBlock() + } + Assert.assertEquals(1, orders.findAllWithExposedTrxBlock().size) + Assert.assertEquals(2, payments.findAllWithExposedTrxBlock().size) + } + + @Test + open fun test9() { + kotlin.runCatching { + orders.transaction { + orders.createWithExposedTrxBlock() + payments.createWithExposedTrxBlock() + throw SpringTransactionTestException() + } + } + Assert.assertEquals(0, orders.findAllWithExposedTrxBlock().size) + Assert.assertEquals(1, payments.findAllWithExposedTrxBlock().size) + } + + @Test + open fun test10() { + kotlin.runCatching { + orders.transaction { + orders.createWithExposedTrxBlock() + payments.databaseTemplate { + payments.createWithExposedTrxBlock() + throw SpringTransactionTestException() + } + } + } + Assert.assertEquals(0, orders.findAllWithExposedTrxBlock().size) + Assert.assertEquals(0, payments.findAllWithExposedTrxBlock().size) + } +} + +@Configuration +@EnableTransactionManagement(proxyTargetClass = true) +open class OrderConfig { + + @Bean + open fun dataSource(): EmbeddedDatabase = EmbeddedDatabaseBuilder().setName("embeddedTest1").setType( + EmbeddedDatabaseType.H2 + ).build() + + @Bean + open fun transactionManager(dataSource: DataSource) = SpringTransactionManager(dataSource) + + @Bean + open fun orders() = Orders() +} + +@Transactional +open class Orders { + + open fun findAll(): List = Order.selectAll().toList() + + open fun findAllWithExposedTrxBlock() = org.jetbrains.exposed.sql.transactions.transaction { findAll() } + + open fun create() = Order.insertAndGetId { + it[buyer] = 123 + }.value + + open fun createWithExposedTrxBlock() = org.jetbrains.exposed.sql.transactions.transaction { create() } + + open fun init() { + SchemaUtils.create(Order) + Order.deleteAll() + } + + open fun transaction(block: () -> Unit) { + block() + } +} + +object Order : LongIdTable("orders") { + val buyer = long("buyer_id") +} + +@Configuration +@EnableTransactionManagement(proxyTargetClass = true) +open class PaymentConfig { + + @Bean + open fun dataSource(): EmbeddedDatabase = EmbeddedDatabaseBuilder().setName("embeddedTest2").setType( + EmbeddedDatabaseType.H2 + ).build() + + @Bean + open fun transactionManager(dataSource: DataSource) = SpringTransactionManager(dataSource) + + @Bean + open fun payments() = Payments() +} + +@Transactional +open class Payments { + + open fun findAll(): List = Payment.selectAll().toList() + + open fun findAllWithExposedTrxBlock() = transaction { findAll() } + + open fun create() = Payment.insertAndGetId { + it[state] = "state" + }.value + + open fun createWithExposedTrxBlock() = transaction { create() } + + open fun init() { + SchemaUtils.create(Payment) + Payment.deleteAll() + } + + open fun databaseTemplate(block: () -> Unit) { + block() + } +} + +object Payment : LongIdTable("payments") { + val state = varchar("state", 50) +} + +private class SpringTransactionTestException : Error() diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionEntityTest.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionEntityTest.kt index 7718696539..959971aa87 100644 --- a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionEntityTest.kt +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionEntityTest.kt @@ -5,12 +5,13 @@ import org.jetbrains.exposed.dao.UUIDEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.annotation.Commit import org.springframework.transaction.annotation.Transactional import java.util.* +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -39,6 +40,11 @@ class OrderDAO(id: EntityID) : UUIDEntity(id) { @org.springframework.stereotype.Service @Transactional open class Service { + + open fun init() { + SchemaUtils.create(CustomerTable, OrderTable) + } + open fun createCustomer(name: String): CustomerDAO { return CustomerDAO.new { this.name = name @@ -59,6 +65,14 @@ open class Service { open fun findOrderByProduct(product: String): OrderDAO? { return OrderDAO.find { OrderTable.product eq product }.singleOrNull() } + + open fun transaction(block: () -> Unit) { + block() + } + + open fun cleanUp() { + SchemaUtils.drop(CustomerTable, OrderTable) + } } open class SpringTransactionEntityTest : SpringTransactionTestBase() { @@ -66,29 +80,36 @@ open class SpringTransactionEntityTest : SpringTransactionTestBase() { @Autowired lateinit var service: Service - @Test @Commit - open fun test01() { - transaction { - SchemaUtils.create(CustomerTable, OrderTable) - } + @BeforeTest + open fun beforeTest() { + service.init() + } + @Test + @Commit + open fun test01() { val customer = service.createCustomer("Alice1") service.createOrder(customer, "Product1") val order = service.findOrderByProduct("Product1") assertNotNull(order) - transaction { + service.transaction { assertEquals("Alice1", order.customer.name) } } - @Test @Commit + @Test + @Commit fun test02() { service.doBoth("Bob", "Product2") val order = service.findOrderByProduct("Product2") assertNotNull(order) - transaction { + service.transaction { assertEquals("Bob", order.customer.name) - SchemaUtils.drop(CustomerTable, OrderTable) } } + + @AfterTest + fun afterTest() { + service.cleanUp() + } } diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionManagerTest.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionManagerTest.kt new file mode 100644 index 0000000000..5c6240064f --- /dev/null +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionManagerTest.kt @@ -0,0 +1,386 @@ +package org.jetbrains.exposed.spring + +import junit.framework.TestCase.assertEquals +import org.jetbrains.exposed.sql.DatabaseConfig +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.junit.Test +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy +import org.springframework.transaction.IllegalTransactionStateException +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.TransactionStatus +import org.springframework.transaction.TransactionSystemException +import org.springframework.transaction.support.TransactionTemplate +import java.sql.SQLException +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class SpringTransactionManagerTest { + + private val ds1 = DataSourceSpy(::ConnectionSpy) + private val con1 = ds1.con as ConnectionSpy + private val ds2 = DataSourceSpy(::ConnectionSpy) + private val con2 = ds2.con as ConnectionSpy + + @BeforeTest + fun beforeTest() { + con1.clearMock() + con2.clearMock() + } + + @AfterTest + fun afterTest() { + while (TransactionManager.defaultDatabase != null) { + TransactionManager.defaultDatabase?.let { TransactionManager.closeAndUnregister(it) } + } + } + + @Test + fun `set manager when transaction start`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert(false) + } + + @Test + fun `set right transaction manager when two transaction manager exist`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert(false) + + val tm2 = SpringTransactionManager(ds2) + tm2.executeAssert(false) + } + + @Test + fun `set right transaction manager when two transaction manager with nested transaction template`() { + val tm = SpringTransactionManager(ds1) + val tm2 = SpringTransactionManager(ds2) + + tm2.executeAssert(false) { + tm.executeAssert(false) + assertEquals( + TransactionManager.managerFor(TransactionManager.currentOrNull()?.db), + TransactionManager.manager + ) + } + } + + @Test + fun `connection commit and close when transaction success`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert() + + assertTrue(con1.verifyCallOrder("setReadOnly", "setAutoCommit", "commit", "close")) + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `connection rollback and close when transaction fail`() { + val tm = SpringTransactionManager(ds1) + val ex = RuntimeException("Application exception") + try { + tm.executeAssert { + throw ex + } + } catch (e: Exception) { + assertEquals(ex, e) + } + assertEquals(1, con1.rollbackCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `connection commit and closed when nested transaction success`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert { + tm.executeAssert() + } + + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `connection commit and closed when two different transaction manager with nested transaction success`() { + val tm1 = SpringTransactionManager(ds1) + val tm2 = SpringTransactionManager(ds2) + + tm1.executeAssert { + tm2.executeAssert() + assertEquals( + TransactionManager.managerFor(TransactionManager.currentOrNull()?.db), + TransactionManager.manager + ) + } + + assertEquals(1, con2.commitCallCount) + assertEquals(1, con2.closeCallCount) + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `connection rollback and closed when two different transaction manager with nested transaction failed`() { + val tm1 = SpringTransactionManager(ds1) + val tm2 = SpringTransactionManager(ds2) + val ex = RuntimeException("Application exception") + try { + tm1.executeAssert { + tm2.executeAssert { + throw ex + } + assertEquals( + TransactionManager.managerFor(TransactionManager.currentOrNull()?.db), + TransactionManager.manager + ) + } + } catch (e: Exception) { + assertEquals(ex, e) + } + + assertEquals(0, con2.commitCallCount) + assertEquals(1, con2.rollbackCallCount) + assertEquals(1, con2.closeCallCount) + assertEquals(0, con1.commitCallCount) + assertEquals(1, con1.rollbackCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `transaction commit with lazy connection data source proxy`() { + val lazyDs = LazyConnectionDataSourceProxy(ds1) + val tm = SpringTransactionManager(lazyDs) + tm.executeAssert() + + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `transaction rollback with lazy connection data source proxy`() { + val lazyDs = LazyConnectionDataSourceProxy(ds1) + val tm = SpringTransactionManager(lazyDs) + val ex = RuntimeException("Application exception") + try { + tm.executeAssert { + throw ex + } + } catch (e: Exception) { + assertEquals(ex, e) + } + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `transaction commit with transaction aware data source proxy`() { + val transactionAwareDs = TransactionAwareDataSourceProxy(ds1) + val tm = SpringTransactionManager(transactionAwareDs) + tm.executeAssert() + + assertTrue(con1.verifyCallOrder("setReadOnly", "setAutoCommit", "commit")) + assertEquals(1, con1.commitCallCount) + assertTrue(con1.closeCallCount > 0) + } + + @Test + fun `transaction rollback with transaction aware data source proxy`() { + val transactionAwareDs = TransactionAwareDataSourceProxy(ds1) + val tm = SpringTransactionManager(transactionAwareDs) + val ex = RuntimeException("Application exception") + try { + tm.executeAssert { + throw ex + } + } catch (e: Exception) { + assertEquals(ex, e) + } + + assertTrue(con1.verifyCallOrder("setReadOnly", "setAutoCommit", "rollback")) + assertEquals(1, con1.rollbackCallCount) + assertTrue(con1.closeCallCount > 0) + } + + @Test + fun `transaction exception on commit and rollback on commit failure`() { + con1.mockCommit = { throw SQLException("Commit failure") } + + val tm = SpringTransactionManager(ds1) + tm.isRollbackOnCommitFailure = true + assertFailsWith { + tm.executeAssert() + } + + assertTrue(con1.verifyCallOrder("setReadOnly", "setAutoCommit", "commit", "isClosed", "rollback", "close")) + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.rollbackCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `transaction with exception on rollback`() { + con1.mockRollback = { throw SQLException("Rollback failure") } + + val tm = SpringTransactionManager(ds1) + assertFailsWith { + tm.executeAssert { + assertEquals(false, it.isRollbackOnly) + it.setRollbackOnly() + assertEquals(true, it.isRollbackOnly) + } + } + + assertTrue(con1.verifyCallOrder("setReadOnly", "setAutoCommit", "isClosed", "rollback", "close")) + assertEquals(1, con1.rollbackCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `nested transaction with commit`() { + val tm = SpringTransactionManager(ds1, DatabaseConfig { useNestedTransactions = true }) + + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_NESTED) { + assertTrue(it.isNewTransaction) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_NESTED) + assertTrue(it.isNewTransaction) + } + + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `nested transaction with rollback`() { + val tm = SpringTransactionManager(ds1, DatabaseConfig { useNestedTransactions = true }) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_NESTED) { + assertTrue(it.isNewTransaction) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_NESTED) { status -> + status.setRollbackOnly() + } + assertTrue(it.isNewTransaction) + } + + assertEquals(1, con1.rollbackCallCount) + assertEquals(1, con1.releaseSavepointCallCount) + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `requires new with commit`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert { + assertTrue(it.isNewTransaction) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW) { status -> + assertTrue(status.isNewTransaction) + } + assertTrue(it.isNewTransaction) + } + + assertEquals(2, con1.commitCallCount) + assertEquals(2, con1.closeCallCount) + } + + @Test + fun `requires new with inner rollback`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert { + assertTrue(it.isNewTransaction) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW) { status -> + assertTrue(status.isNewTransaction) + status.setRollbackOnly() + } + assertTrue(it.isNewTransaction) + } + + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.rollbackCallCount) + assertEquals(2, con1.closeCallCount) + } + + @Test + fun `not support with required transaction`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert { + assertTrue(it.isNewTransaction) + tm.executeAssert( + initializeConnection = false, + propagationBehavior = TransactionDefinition.PROPAGATION_NOT_SUPPORTED + ) { + assertFailsWith { + TransactionManager.current().connection + } + } + assertTrue(it.isNewTransaction) + TransactionManager.current().connection + } + + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `mandatory with transaction`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert { + assertTrue(it.isNewTransaction) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_MANDATORY) + assertTrue(it.isNewTransaction) + TransactionManager.current().connection + } + + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `mandatory without transaction`() { + val tm = SpringTransactionManager(ds1) + assertFailsWith { + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_MANDATORY) + } + } + + @Test + fun `support with transaction`() { + val tm = SpringTransactionManager(ds1) + tm.executeAssert { + assertTrue(it.isNewTransaction) + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_SUPPORTS) + assertTrue(it.isNewTransaction) + TransactionManager.current().connection + } + + assertEquals(1, con1.commitCallCount) + assertEquals(1, con1.closeCallCount) + } + + @Test + fun `support without transaction`() { + val tm = SpringTransactionManager(ds1) + assertFailsWith { + tm.executeAssert(propagationBehavior = TransactionDefinition.PROPAGATION_SUPPORTS) + } + tm.executeAssert(initializeConnection = false, propagationBehavior = TransactionDefinition.PROPAGATION_SUPPORTS) + assertEquals(0, con1.commitCallCount) + assertEquals(0, con1.rollbackCallCount) + assertEquals(0, con1.closeCallCount) + } + + private fun SpringTransactionManager.executeAssert( + initializeConnection: Boolean = true, + propagationBehavior: Int = TransactionDefinition.PROPAGATION_REQUIRED, + body: (TransactionStatus) -> Unit = {} + ) { + val tt = TransactionTemplate(this) + tt.propagationBehavior = propagationBehavior + tt.executeWithoutResult { + assertEquals( + TransactionManager.managerFor(TransactionManager.currentOrNull()?.db), + TransactionManager.manager + ) + if (initializeConnection) TransactionManager.current().connection + body(it) + } + } +} diff --git a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionTestBase.kt b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionTestBase.kt index ffdb528ba1..db5d710420 100644 --- a/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionTestBase.kt +++ b/spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionTestBase.kt @@ -23,10 +23,15 @@ import org.springframework.transaction.annotation.TransactionManagementConfigure open class TestConfig : TransactionManagementConfigurer { @Bean - open fun ds(): EmbeddedDatabase = EmbeddedDatabaseBuilder().setName("embeddedTest").setType(EmbeddedDatabaseType.H2).build() + open fun ds(): EmbeddedDatabase = EmbeddedDatabaseBuilder().setName( + "embeddedTest" + ).setType(EmbeddedDatabaseType.H2).build() @Bean - override fun annotationDrivenTransactionManager(): PlatformTransactionManager = SpringTransactionManager(ds(), DatabaseConfig {}) + override fun annotationDrivenTransactionManager(): PlatformTransactionManager = SpringTransactionManager( + ds(), + DatabaseConfig { useNestedTransactions = true } + ) @Bean open fun service(): Service = Service() @@ -38,8 +43,12 @@ open class TestConfig : TransactionManagementConfigurer { @RunWith(SpringJUnit4ClassRunner::class) @ContextConfiguration(classes = [TestConfig::class]) @FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Suppress("UnnecessaryAbstractClass") abstract class SpringTransactionTestBase { @Autowired lateinit var ctx: ApplicationContext + + @Autowired + lateinit var transactionManager: PlatformTransactionManager }